Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117737684
D1810.1775153280.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
79 KB
Referenced Files
None
Subscribers
None
D1810.1775153280.diff
View Options
diff --git a/bin/quickstart.sh b/bin/quickstart.sh
--- a/bin/quickstart.sh
+++ b/bin/quickstart.sh
@@ -78,6 +78,8 @@
rm -rf database/database.sqlite
./artisan db:ping --wait
php -dmemory_limit=512M ./artisan migrate:refresh --seed
-./artisan serve
+./artisan data:import
+./artisan swoole:http stop >/dev/null 2>&1 || :
+./artisan swoole:http start
popd
diff --git a/extras/kolab_policy_greylist.py b/extras/kolab_policy_greylist.py
new file mode 100755
--- /dev/null
+++ b/extras/kolab_policy_greylist.py
@@ -0,0 +1,89 @@
+#!/usr/bin/python3
+"""
+An example implementation of a policy service.
+"""
+
+import json
+import time
+import sys
+
+import requests
+
+
+def read_request_input():
+ """
+ Read a single policy request from sys.stdin, and return a dictionary
+ containing the request.
+ """
+ start_time = time.time()
+
+ policy_request = {}
+ end_of_request = False
+
+ while not end_of_request:
+ if (time.time() - start_time) >= 10:
+ sys.exit(0)
+
+ request_line = sys.stdin.readline()
+
+ if request_line.strip() == '':
+ if 'request' in policy_request:
+ end_of_request = True
+ else:
+ request_line = request_line.strip()
+ request_key = request_line.split('=')[0]
+ request_value = '='.join(request_line.split('=')[1:])
+
+ policy_request[request_key] = request_value
+
+ return policy_request
+
+
+if __name__ == "__main__":
+ TOKEN = 'abcdef'
+ # URL = 'https://services.kolabnow.com/api/webhooks/greylist'
+ # URL = 'http://127.0.0.1:8000/api/webhooks/greylist'
+ URL = 'https://kanarip.dev.kolab.io/api/webhooks/greylist'
+
+ # Start the work
+ while True:
+ REQUEST = read_request_input()
+
+ # print("timestamp={0}".format(REQUEST['timestamp']))
+
+ try:
+ RESPONSE = requests.post(
+ URL,
+ data=REQUEST,
+ headers={'X-Token': TOKEN},
+ verify=True
+ )
+ # pylint: disable=broad-except
+ except Exception:
+ print("action=DEFER_IF_PERMIT Temporary error, try again later.")
+ sys.exit(1)
+
+ try:
+ R = json.loads(RESPONSE.text)
+ # pylint: disable=broad-except
+ except Exception:
+ sys.exit(1)
+
+ if 'prepend' in R:
+ for prepend in R['prepend']:
+ print("action=PREPEND {0}".format(prepend))
+
+ if RESPONSE.ok:
+ print(
+ "action={0}\n".format(R['response'])
+ )
+
+ sys.stdout.flush()
+ else:
+ print(
+ "action={0} {1}\n".format(R['response'], R['reason'])
+ )
+
+ sys.stdout.flush()
+
+ sys.exit(0)
diff --git a/extras/kolab_policy_spf.py b/extras/kolab_policy_spf.py
new file mode 100755
--- /dev/null
+++ b/extras/kolab_policy_spf.py
@@ -0,0 +1,90 @@
+#!/usr/bin/python3
+"""
+This is the implementation of a (postfix) MTA policy service to enforce the
+Sender Policy Framework.
+"""
+
+import json
+import time
+import sys
+
+import requests
+
+
+def read_request_input():
+ """
+ Read a single policy request from sys.stdin, and return a dictionary
+ containing the request.
+ """
+ start_time = time.time()
+
+ policy_request = {}
+ end_of_request = False
+
+ while not end_of_request:
+ if (time.time() - start_time) >= 10:
+ sys.exit(0)
+
+ request_line = sys.stdin.readline()
+
+ if request_line.strip() == '':
+ if 'request' in policy_request:
+ end_of_request = True
+ else:
+ request_line = request_line.strip()
+ request_key = request_line.split('=')[0]
+ request_value = '='.join(request_line.split('=')[1:])
+
+ policy_request[request_key] = request_value
+
+ return policy_request
+
+
+if __name__ == "__main__":
+ TOKEN = 'abcdef'
+ # URL = 'https://services.kolabnow.com/api/webhooks/spf'
+ # URL = 'http://127.0.0.1:8000/api/webhooks/spf'
+ URL = 'https://kanarip.dev.kolab.io/api/webhooks/spf'
+
+ # Start the work
+ while True:
+ REQUEST = read_request_input()
+
+ # print("timestamp={0}".format(REQUEST['timestamp']))
+
+ try:
+ RESPONSE = requests.post(
+ URL,
+ data=REQUEST,
+ headers={'X-Token': TOKEN},
+ verify=True
+ )
+ # pylint: disable=broad-except
+ except Exception:
+ print("action=DEFER_IF_PERMIT Temporary error, try again later.")
+ sys.exit(1)
+
+ try:
+ R = json.loads(RESPONSE.text)
+ # pylint: disable=broad-except
+ except Exception:
+ sys.exit(1)
+
+ if 'prepend' in R:
+ for prepend in R['prepend']:
+ print("action=PREPEND {0}".format(prepend))
+
+ if RESPONSE.ok:
+ print(
+ "action={0}\n".format(R['response'])
+ )
+
+ sys.stdout.flush()
+ else:
+ print(
+ "action={0} {1}\n".format(R['response'], R['reason'])
+ )
+
+ sys.stdout.flush()
+
+ sys.exit(0)
diff --git a/src/.gitignore b/src/.gitignore
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -7,6 +7,8 @@
public/js/*.js
public/storage/
storage/*.key
+storage/*.log
+storage/*-????-??-??*
storage/export/
tests/report/
vendor
diff --git a/src/app/Console/Commands/User/GreylistCommand.php b/src/app/Console/Commands/User/GreylistCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/User/GreylistCommand.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Console\Commands\User;
+
+use App\Console\Command;
+
+class GreylistCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:greylist {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'List currently greylisted delivery attempts for the user.';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ // pretend that all users are local;
+ $recipientAddress = $this->argument('user');
+ $recipientHash = hash('sha256', $recipientAddress);
+
+ $lastConnect = \App\Greylist\Connect::where('recipient_hash', $recipientHash)
+ ->orderBy('updated_at', 'desc')
+ ->first();
+
+ if ($lastConnect) {
+ $timestamp = $lastConnect->updated_at->copy();
+ $this->info("Going from timestamp (last connect) {$timestamp}");
+ } else {
+ $timestamp = \Carbon\Carbon::now();
+ $this->info("Going from timestamp (now) {$timestamp}");
+ }
+
+
+ \App\Greylist\Connect::where('recipient_hash', $recipientHash)
+ ->where('greylisting', true)
+ ->whereDate('updated_at', '>=', $timestamp->copy()->subDays(7))
+ ->orderBy('created_at')->each(
+ function ($connect) {
+ $this->info(
+ sprintf(
+ "From %s@%s since %s",
+ $connect->sender_local,
+ $connect->sender_domain,
+ $connect->created_at
+ )
+ );
+ }
+ );
+ }
+}
diff --git a/src/app/Greylist/Connect.php b/src/app/Greylist/Connect.php
new file mode 100644
--- /dev/null
+++ b/src/app/Greylist/Connect.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Greylist;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * @property \App\Domain $domain
+ * @property \App\Domain|\App\User $recipient
+ * @property \App\User $user
+ */
+class Connect extends Model
+{
+ protected $fillable = [
+ 'sender_local',
+ 'sender_domain',
+ 'net_id',
+ 'net_type',
+ 'recipient_hash',
+ 'recipient_id',
+ 'recipient_type',
+ 'connect_count',
+ 'created_at',
+ 'updated_at'
+ ];
+
+ protected $table = 'greylist_connect';
+
+ public function domain()
+ {
+ if ($this->recipient_type == \App\Domain::class) {
+ return $this->recipient;
+ }
+
+ return null;
+ }
+
+ // determine if the sender is a penpal of the recipient.
+ public function isPenpal()
+ {
+ return false;
+ }
+
+ public function user()
+ {
+ if ($this->recipient_type == \App\User::class) {
+ return $this->recipient;
+ }
+
+ return null;
+ }
+
+ public function net()
+ {
+ return $this->morphTo();
+ }
+
+ public function recipient()
+ {
+ return $this->morphTo();
+ }
+}
diff --git a/src/app/Greylist/Request.php b/src/app/Greylist/Request.php
new file mode 100644
--- /dev/null
+++ b/src/app/Greylist/Request.php
@@ -0,0 +1,318 @@
+<?php
+
+namespace App\Greylist;
+
+use Illuminate\Support\Facades\DB;
+
+class Request
+{
+ protected $header;
+ protected $netID;
+ protected $netType;
+ protected $recipientHash;
+ protected $recipientID = null;
+ protected $recipientType = null;
+ protected $sender;
+ protected $senderLocal;
+ protected $senderDomain;
+ protected $timestamp;
+ protected $whitelist;
+ protected $request = [];
+
+ public function __construct($request)
+ {
+ $this->request = $request;
+
+ if (array_key_exists('timestamp', $this->request)) {
+ $this->timestamp = \Carbon\Carbon::parse($this->request['timestamp']);
+ } else {
+ $this->timestamp = \Carbon\Carbon::now();
+ }
+ }
+
+ public function headerGreylist()
+ {
+ if ($this->whitelist) {
+ if ($this->whitelist->sender_local) {
+ return sprintf(
+ "Received-Greylist: sender %s whitelisted since %s",
+ $this->sender,
+ $this->whitelist->created_at->toDateString()
+ );
+ }
+
+ return sprintf(
+ "Received-Greylist: domain %s from %s whitelisted since %s (UTC)",
+ $this->senderDomain,
+ $this->request['client_address'],
+ $this->whitelist->created_at->toDateTimeString()
+ );
+ }
+
+ $connect = $this->findConnectsCollection()->orderBy('created_at')->first();
+
+ if ($connect) {
+ return sprintf(
+ "Received-Greylist: greylisted from %s until %s.",
+ $connect->created_at,
+ $this->timestamp
+ );
+ }
+
+ return "Received-Greylist: no opinion here";
+ }
+
+ public function shouldDefer()
+ {
+ $deferIfPermit = true;
+
+ list($this->netID, $this->netType) = $this->clientNetFromRequest();
+
+ if (!$this->netID) {
+ return true;
+ }
+
+ $recipient = $this->recipientFromRequest();
+
+ $this->sender = $this->senderFromRequest();
+
+ list($this->senderLocal, $this->senderDomain) = explode('@', $this->sender);
+
+ $entry = $this->findConnectsCollectionRecent()->orderBy('updated_at')->first();
+
+ if (!$entry) {
+ // purge all entries to avoid a unique constraint violation.
+ $this->findConnectsCollection()->delete();
+
+ $entry = \App\Greylist\Connect::create(
+ [
+ 'sender_local' => $this->senderLocal,
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType,
+ 'recipient_hash' => $this->recipientHash,
+ 'recipient_id' => $this->recipientID,
+ 'recipient_type' => $this->recipientType,
+ 'connect_count' => 1,
+ 'created_at' => $this->timestamp,
+ 'updated_at' => $this->timestamp
+ ]
+ );
+ }
+
+ // see if all recipients and their domains are opt-outs
+ $enabled = false;
+
+ if ($recipient) {
+ $setting = \App\Greylist\Setting::where(
+ [
+ 'object_id' => $this->recipientID,
+ 'object_type' => $this->recipientType,
+ 'key' => 'greylist_enabled'
+ ]
+ )->first();
+
+ if (!$setting) {
+ $setting = \App\Greylist\Setting::where(
+ [
+ 'object_id' => $recipient->domain()->id,
+ 'object_type' => \App\Domain::class,
+ 'key' => 'greylist_enabled'
+ ]
+ )->first();
+
+ if (!$setting) {
+ $enabled = true;
+ } else {
+ if ($setting->{'value'} !== 'false') {
+ $enabled = true;
+ }
+ }
+ } else {
+ if ($setting->{'value'} !== 'false') {
+ $enabled = true;
+ }
+ }
+ } else {
+ $enabled = true;
+ }
+
+ // the following block is to maintain statistics and state ...
+ $entries = \App\Greylist\Connect::where(
+ [
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType
+ ]
+ )
+ ->whereDate('updated_at', '>=', $this->timestamp->copy()->subDays(7));
+
+ // determine if the sender domain is a whitelist from this network
+ $this->whitelist = \App\Greylist\Whitelist::where(
+ [
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType
+ ]
+ )->first();
+
+ if ($this->whitelist) {
+ if ($this->whitelist->updated_at < $this->timestamp->copy()->subMonthsWithoutOverflow(1)) {
+ $this->whitelist->delete();
+ } else {
+ $this->whitelist->updated_at = $this->timestamp;
+ $this->whitelist->save(['timestamps' => false]);
+
+ $entries->update(
+ [
+ 'greylisting' => false,
+ 'updated_at' => $this->timestamp
+ ]
+ );
+
+ return false;
+ }
+ } else {
+ if ($entries->count() >= 5) {
+ $this->whitelist = \App\Greylist\Whitelist::create(
+ [
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType,
+ 'created_at' => $this->timestamp,
+ 'updated_at' => $this->timestamp
+ ]
+ );
+
+ $entries->update(
+ [
+ 'greylisting' => false,
+ 'updated_at' => $this->timestamp
+ ]
+ );
+ }
+ }
+
+ // TODO: determine if the sender (individual) is a whitelist
+
+ // TODO: determine if the sender is a penpal of any of the recipients. First recipient wins.
+
+ if (!$enabled) {
+ return false;
+ }
+
+ // determine if the sender, net and recipient combination has existed before, for each recipient
+ // any one recipient matching should supersede the other recipients not having matched
+ $connect = \App\Greylist\Connect::where(
+ [
+ 'sender_local' => $this->senderLocal,
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType,
+ 'recipient_id' => $this->recipientID,
+ 'recipient_type' => $this->recipientType,
+ ]
+ )
+ ->whereDate('updated_at', '>=', $this->timestamp->copy()->subMonthsWithoutOverflow(1))
+ ->orderBy('updated_at')
+ ->first();
+
+ if (!$connect) {
+ $connect = \App\Greylist\Connect::create(
+ [
+ 'sender_local' => $this->senderLocal,
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType,
+ 'recipient_id' => $this->recipientID,
+ 'recipient_type' => $this->recipientType,
+ 'connect_count' => 0,
+ 'created_at' => $this->timestamp,
+ 'updated_at' => $this->timestamp
+ ]
+ );
+ }
+
+ $connect->connect_count += 1;
+
+ // TODO: The period of time for which the greylisting persists is configurable.
+ if ($connect->created_at < $this->timestamp->copy()->subMinutes(5)) {
+ $deferIfPermit = false;
+
+ $connect->greylisting = false;
+ }
+
+ $connect->save();
+
+ return $deferIfPermit;
+ }
+
+ private function clientNetFromRequest()
+ {
+ if (strpos($this->request['client_address'], ':') === false) {
+ $net = \App\IP4Net::getNet($this->request['client_address']);
+
+ if ($net) {
+ return [$net->id, \App\IP4Net::class];
+ }
+ } else {
+ $net = \App\IP6Net::getNet($this->request['client_address']);
+
+ if ($net) {
+ return [$net->id, \App\IP6Net::class];
+ }
+ }
+
+ return [null, null];
+ }
+
+ private function findConnectsCollection()
+ {
+ $collection = \App\Greylist\Connect::where(
+ [
+ 'sender_local' => $this->senderLocal,
+ 'sender_domain' => $this->senderDomain,
+ 'net_id' => $this->netID,
+ 'net_type' => $this->netType,
+ 'recipient_id' => $this->recipientID,
+ 'recipient_type' => $this->recipientType
+ ]
+ );
+
+ return $collection;
+ }
+
+ private function findConnectsCollectionRecent()
+ {
+ return $this->findConnectsCollection()
+ ->where('updated_at', '>=', $this->timestamp->copy()->subDays(7));
+ }
+
+ private function recipientFromRequest()
+ {
+ $recipients = \App\Utils::findObjectsByRecipientAddress($this->request['recipient']);
+
+ if (sizeof($recipients) > 1) {
+ \Log::warning(
+ "Only taking the first recipient from the request in to account for {$this->request['recipient']}"
+ );
+ }
+
+ if (count($recipients) >= 1) {
+ $recipient = $recipients[0];
+ $this->recipientID = $recipient->id;
+ $this->recipientType = get_class($recipient);
+ } else {
+ $recipient = null;
+ }
+
+ $this->recipientHash = hash('sha256', $this->request['recipient']);
+
+ return $recipient;
+ }
+
+ public function senderFromRequest()
+ {
+ return \App\Utils::normalizeAddress($this->request['sender']);
+ }
+}
diff --git a/src/app/Greylist/Setting.php b/src/app/Greylist/Setting.php
new file mode 100644
--- /dev/null
+++ b/src/app/Greylist/Setting.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Greylist;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Setting extends Model
+{
+ protected $table = 'greylist_settings';
+
+ protected $fillable = [
+ 'object_id',
+ 'object_type',
+ 'key',
+ 'value'
+ ];
+}
diff --git a/src/app/Greylist/Whitelist.php b/src/app/Greylist/Whitelist.php
new file mode 100644
--- /dev/null
+++ b/src/app/Greylist/Whitelist.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Greylist;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Whitelist extends Model
+{
+ protected $table = 'greylist_whitelist';
+
+ protected $fillable = [
+ 'sender_local',
+ 'sender_domain',
+ 'net_id',
+ 'net_type',
+ 'created_at',
+ 'updated_at'
+ ];
+}
diff --git a/src/app/Http/Controllers/API/V4/PolicyController.php b/src/app/Http/Controllers/API/V4/PolicyController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/PolicyController.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\Controller;
+use App\Providers\PaymentProvider;
+use App\Wallet;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Validator;
+
+class PolicyController extends Controller
+{
+ /**
+ * Take a greylist policy request
+ *
+ * @return \Illuminate\Http\Response The response
+ */
+ public function greylist()
+ {
+ $data = \request()->input();
+
+ list($local, $domainName) = explode('@', $data['recipient']);
+
+ $request = new \App\Greylist\Request($data);
+
+ $shouldDefer = $request->shouldDefer();
+
+ if ($shouldDefer) {
+ return response()->json(
+ ['response' => 'DEFER_IF_PERMIT', 'reason' => "Greylisted for 5 minutes. Try again later."],
+ 403
+ );
+ }
+
+ $prependGreylist = $request->headerGreylist();
+
+ $result = [
+ 'response' => 'DUNNO',
+ 'prepend' => [$prependGreylist]
+ ];
+
+ return response()->json($result, 200);
+ }
+
+ /*
+ * Apply the sender policy framework to a request.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function senderPolicyFramework()
+ {
+ $data = \request()->input();
+
+ list($netID, $netType) = $this->clientNetFromRequest($data['client_address']);
+ list($senderLocal, $senderDomain) = explode('@', $data['sender']);
+
+ // This network can not be recognized.
+ if (!$netID) {
+ return response()->json(
+ [
+ 'response' => 'DEFER_IF_PERMIT',
+ 'reason' => 'Temporary error. Please try again later.'
+ ],
+ 403
+ );
+ }
+
+ // Compose the cache key we want.
+ $cacheKey = "{$netType}_{$netID}_{$senderDomain}";
+
+ $result = \App\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);
+
+ \App\SPF\Cache::set($cacheKey, serialize($result));
+ } else {
+ $result = unserialize($result);
+ }
+
+ $fail = false;
+
+ 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 = \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 = call_user_func_array(
+ [$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);
+ }
+
+ private function clientNetFromRequest($clientAddress)
+ {
+ if (strpos($clientAddress, ':') === false) {
+ $net = \App\IP4Net::getNet($clientAddress);
+
+ if ($net) {
+ return [$net->id, "ip4"];
+ }
+ } else {
+ $net = \App\IP6Net::getNet($clientAddress);
+
+ if ($net) {
+ return [$net->id, "ip6"];
+ }
+ }
+
+ return [null, null];
+ }
+}
diff --git a/src/app/Http/Kernel.php b/src/app/Http/Kernel.php
--- a/src/app/Http/Kernel.php
+++ b/src/app/Http/Kernel.php
@@ -40,7 +40,7 @@
],
'api' => [
- 'throttle:120,1',
+ //'throttle:120,1',
'bindings',
],
];
diff --git a/src/app/IP4Net.php b/src/app/IP4Net.php
--- a/src/app/IP4Net.php
+++ b/src/app/IP4Net.php
@@ -10,10 +10,31 @@
protected $table = "ip4nets";
protected $fillable = [
+ 'rir_name',
'net_number',
'net_mask',
'net_broadcast',
'country',
- 'serial'
+ 'serial',
+ 'created_at',
+ 'updated_at'
];
+
+ public static function getNet($ip, $mask = 32)
+ {
+ $query = "
+ SELECT id FROM ip4nets
+ WHERE INET_ATON(net_number) <= INET_ATON(?)
+ AND INET_ATON(net_broadcast) >= INET_ATON(?)
+ ORDER BY INET_ATON(net_number), net_mask DESC LIMIT 1
+ ";
+
+ $results = DB::select($query, [$ip, $ip]);
+
+ if (sizeof($results) == 0) {
+ return null;
+ }
+
+ return \App\IP4Net::find($results[0]->id);
+ }
}
diff --git a/src/app/SPF/Cache.php b/src/app/SPF/Cache.php
new file mode 100644
--- /dev/null
+++ b/src/app/SPF/Cache.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\SPF;
+
+use Illuminate\Support\Facades\Cache as LaravelCache;
+
+/**
+ * A caching layer for SPF check results, as sometimes the chasing of DNS entries can take a while but submissions
+ * inbound are virtually not rate-limited.
+ *
+ * A cache key should have the format of ip(4|6)_id_domain and last for 12 hours.
+ *
+ * A cache value should have a serialized version of the \SPFLib\Checker.
+ */
+class Cache
+{
+ public static function get($key)
+ {
+ if (LaravelCache::has($key)) {
+ return LaravelCache::get($key);
+ }
+
+ return null;
+ }
+
+ public static function has($key)
+ {
+ return LaravelCache::has($key);
+ }
+
+ public static function set($key, $value)
+ {
+ if (LaravelCache::has($key)) {
+ LaravelCache::forget($key);
+ }
+
+ // cache the DNS record result for 12 hours
+ LaravelCache::put($key, $value, 60 * 60 * 12);
+ }
+}
diff --git a/src/app/SPF/Policy.php b/src/app/SPF/Policy.php
new file mode 100644
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -574,6 +574,46 @@
return $this;
}
+ public function senderPolicyFrameworkWhitelist($clientName)
+ {
+ $setting = $this->getSetting('spf_whitelist');
+
+ if (!$setting) {
+ return false;
+ }
+
+ $whitelist = json_decode($setting);
+
+ $matchFound = false;
+
+ foreach ($whitelist as $entry) {
+ if (substr($entry, 0, 1) == '/') {
+ $match = preg_match($entry, $clientName);
+
+ if ($match) {
+ $matchFound = true;
+ }
+
+ continue;
+ }
+
+ if (substr($entry, 0, 1) == '.') {
+ if (substr($clientName, (-1 * strlen($entry))) == $entry) {
+ $matchFound = true;
+ }
+
+ continue;
+ }
+
+ if ($entry == $clientName) {
+ $matchFound = true;
+ continue;
+ }
+ }
+
+ return $matchFound;
+ }
+
/**
* Any (additional) properties of this user.
*
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -41,7 +41,7 @@
*/
public static function countryForIP($ip)
{
- if (strpos(':', $ip) === false) {
+ if (strpos($ip, ':') === false) {
$query = "
SELECT country FROM ip4nets
WHERE INET_ATON(net_number) <= INET_ATON(?)
@@ -155,6 +155,81 @@
fclose($fp);
}
+ /**
+ * Find an object that is the recipient for the specified address.
+ *
+ * @param string $address
+ *
+ * @return array
+ */
+ public static function findObjectsByRecipientAddress($address)
+ {
+ $address = \App\Utils::normalizeAddress($address);
+
+ list($local, $domainName) = explode('@', $address);
+
+ $domain = \App\Domain::where('namespace', $domainName)->first();
+
+ if (!$domain) {
+ return [];
+ }
+
+ $user = \App\User::where('email', $address)->first();
+
+ if ($user) {
+ return [$user];
+ }
+
+ $userAliases = \App\UserAlias::where('alias', $address)->get();
+
+ if (count($userAliases) > 0) {
+ $users = [];
+
+ foreach ($userAliases as $userAlias) {
+ $users[] = $userAlias->user;
+ }
+
+ return $users;
+ }
+
+ $userAliases = \App\UserAlias::where('alias', "catchall@{$domain->namespace}")->get();
+
+ if (count($userAliases) > 0) {
+ $users = [];
+
+ foreach ($userAliases as $userAlias) {
+ $users[] = $userAlias->user;
+ }
+
+ return $users;
+ }
+
+ return [];
+ }
+
+ /**
+ * Generate a passphrase. Not intended for use in production, so limited to environments that are not production.
+ *
+ * @return string
+ */
+ public static function generatePassphrase()
+ {
+ $alphaLow = 'abcdefghijklmnopqrstuvwxyz';
+ $alphaUp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $num = '0123456789';
+ $stdSpecial = '~`!@#$%^&*()-_+=[{]}\\|\'";:/?.>,<';
+
+ $source = $alphaLow . $alphaUp . $num . $stdSpecial;
+
+ $result = '';
+
+ for ($x = 0; $x < 16; $x++) {
+ $result .= substr($source, rand(0, (strlen($source) - 1)), 1);
+ }
+
+ return $result;
+ }
+
/**
* Calculate the broadcast address provided a net number and a prefix.
*
@@ -210,6 +285,32 @@
return $lastaddrstr;
}
+ /**
+ * Normalize an email address.
+ *
+ * This means to lowercase and strip components separated with recipient delimiters.
+ *
+ * @param string $address The address to normalize.
+ *
+ * @return string
+ */
+ public static function normalizeAddress($address)
+ {
+ $address = strtolower($address);
+
+ list($local, $domain) = explode('@', $address);
+
+ if (strpos($local, '+') === false) {
+ return "{$local}@{$domain}";
+ }
+
+ $localComponents = explode('+', $local);
+
+ $local = array_pop($localComponents);
+
+ return "{$local}@{$domain}";
+ }
+
/**
* Provide all unique combinations of elements in $input, with order and duplicates irrelevant.
*
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -21,6 +21,7 @@
"kolab/net_ldap3": "dev-master",
"laravel/framework": "6.*",
"laravel/tinker": "^2.4",
+ "mlocati/spf-lib": "^3.0",
"mollie/laravel-mollie": "^2.9",
"morrislaptop/laravel-queue-clear": "^1.2",
"silviolleite/laravelpwa": "^2.0",
diff --git a/src/config/cache.php b/src/config/cache.php
--- a/src/config/cache.php
+++ b/src/config/cache.php
@@ -18,7 +18,7 @@
|
*/
- 'default' => env('CACHE_DRIVER', 'file'),
+ 'default' => env('CACHE_DRIVER', 'redis'),
/*
|--------------------------------------------------------------------------
diff --git a/src/config/database.php b/src/config/database.php
--- a/src/config/database.php
+++ b/src/config/database.php
@@ -122,7 +122,7 @@
'options' => [
'cluster' => env('REDIS_CLUSTER', 'predis'),
- 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
+ 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
],
'default' => [
diff --git a/src/database/migrations/2020_10_18_091319_create_greylist_tables.php b/src/database/migrations/2020_10_18_091319_create_greylist_tables.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_10_18_091319_create_greylist_tables.php
@@ -0,0 +1,121 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateGreylistTables extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'greylist_connect',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->string('sender_local', 256);
+ $table->string('sender_domain', 256);
+ $table->string('recipient_hash', 64);
+ $table->bigInteger('recipient_id')->unsigned()->nullable();
+ $table->string('recipient_type', 16)->nullable();
+ $table->bigInteger('net_id');
+ $table->string('net_type', 16);
+ $table->boolean('greylisting')->default(true);
+ $table->bigInteger('connect_count')->unsigned()->default(1);
+ $table->timestamps();
+
+ /**
+ * Index for recipient request.
+ */
+ $table->index(
+ [
+ 'sender_local',
+ 'sender_domain',
+ 'recipient_hash',
+ 'net_id',
+ 'net_type'
+ ],
+ 'ssrnn_idx'
+ );
+
+ /**
+ * Index for domain whitelist query.
+ */
+ $table->index(
+ [
+ 'sender_domain',
+ 'net_id',
+ 'net_type'
+ ],
+ 'snn_idx'
+ );
+
+ /**
+ * Index for updated_at
+ */
+ $table->index('updated_at');
+
+ $table->unique(
+ ['sender_local', 'sender_domain', 'recipient_hash', 'net_id', 'net_type'],
+ 'ssrnn_unq'
+ );
+ }
+ );
+
+ Schema::create(
+ 'greylist_penpals',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('local_id');
+ $table->string('local_type', 16);
+ $table->string('remote_local', 128);
+ $table->string('remote_domain', 256);
+ $table->timestamps();
+ }
+ );
+
+ Schema::create(
+ 'greylist_settings',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('object_id');
+ $table->string('object_type', 16);
+ $table->string('key', 64);
+ $table->text('value');
+ $table->timestamps();
+ }
+ );
+
+ Schema::create(
+ 'greylist_whitelist',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->string('sender_local', 128)->nullable();
+ $table->string('sender_domain', 256);
+ $table->bigInteger('net_id');
+ $table->string('net_type', 16);
+ $table->timestamps();
+
+ $table->index(['sender_local', 'sender_domain', 'net_id', 'net_type'], 'ssnn_idx');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('greylist_connect');
+ Schema::dropIfExists('greylist_penpals');
+ Schema::dropIfExists('greylist_settings');
+ Schema::dropIfExists('greylist_whitelist');
+ }
+}
diff --git a/src/database/seeds/local/DomainSeeder.php b/src/database/seeds/local/DomainSeeder.php
--- a/src/database/seeds/local/DomainSeeder.php
+++ b/src/database/seeds/local/DomainSeeder.php
@@ -25,7 +25,8 @@
"groupoffice.ch",
"journalistmail.ch",
"legalprivilege.ch",
- "libertymail.co"
+ "libertymail.co",
+ "libertymail.net"
];
foreach ($domains as $domain) {
diff --git a/src/phpunit.xml b/src/phpunit.xml
--- a/src/phpunit.xml
+++ b/src/phpunit.xml
@@ -40,5 +40,6 @@
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
+ <server name="SWOOLE_HTTP_ACCESS_LOG" value="false"/>
</php>
</phpunit>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -85,10 +85,12 @@
Route::group(
[
'domain' => \config('app.domain'),
- 'prefix' => $prefix . 'api/webhooks',
+ 'prefix' => $prefix . 'api/webhooks'
],
function () {
+ Route::post('greylist', 'API\V4\PolicyController@greylist');
Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook');
+ Route::post('spf', 'API\V4\PolicyController@senderPolicyFramework');
}
);
diff --git a/src/tests/Feature/Stories/GreylistTest.php b/src/tests/Feature/Stories/GreylistTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Stories/GreylistTest.php
@@ -0,0 +1,606 @@
+<?php
+
+namespace Tests\Feature\Stories;
+
+use Illuminate\Support\Facades\DB;
+use Tests\TestCase;
+
+class GreylistTest extends TestCase
+{
+ private $requests = [];
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->instance = $this->generateInstanceId();
+ $this->clientAddress = '212.103.80.148';
+
+ $this->net = \App\IP4Net::getNet($this->clientAddress);
+
+ DB::delete("DELETE FROM greylist_connect WHERE sender_domain = 'sender.domain';");
+ DB::delete("DELETE FROM greylist_settings;");
+ DB::delete("DELETE FROM greylist_whitelist WHERE sender_domain = 'sender.domain';");
+ }
+
+ public function tearDown(): void
+ {
+ DB::delete("DELETE FROM greylist_connect WHERE sender_domain = 'sender.domain';");
+ DB::delete("DELETE FROM greylist_settings;");
+ DB::delete("DELETE FROM greylist_whitelist WHERE sender_domain = 'sender.domain';");
+
+ parent::tearDown();
+ }
+
+ public function testWithTimestamp()
+ {
+ $request = new \App\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()
+ {
+ $request = new \App\Greylist\Request(
+ [
+ '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 \App\Greylist\Request(
+ [
+ '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()
+ {
+ $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first();
+
+ $this->assertNull($whitelist);
+
+ for ($i = 0; $i < 5; $i++) {
+ $request = new \App\Greylist\Request(
+ [
+ '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());
+ }
+
+ $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first();
+
+ $this->assertNotNull($whitelist);
+
+ $request = new \App\Greylist\Request(
+ [
+ '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 = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first();
+
+ $this->assertNull($whitelist);
+
+ for ($i = 0; $i < 5; $i++) {
+ $request = new \App\Greylist\Request(
+ [
+ '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());
+ }
+
+ $whitelist = \App\Greylist\Whitelist::where('sender_domain', 'sender.domain')->first();
+
+ $this->assertNotNull($whitelist);
+
+ $request = new \App\Greylist\Request(
+ [
+ '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());
+
+ $whitelist->updated_at = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2);
+ $whitelist->save(['timestamps' => false]);
+
+ $this->assertTrue($request->shouldDefer());
+ }
+
+ // public function testWhitelistUpdate() {}
+
+ public function testNew()
+ {
+ $data = [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress,
+ 'client_name' => 'some.mx'
+ ];
+
+ $response = $this->post('/api/webhooks/greylist', $data);
+
+ $response->assertStatus(403);
+ }
+
+ public function testRetry()
+ {
+ $connect = \App\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 \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testDomainDisabled()
+ {
+ $setting = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainHosted->id,
+ 'object_type' => \App\Domain::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'false'
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testDomainEnabled()
+ {
+ $connect = \App\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' => \App\IP4Net::getNet('212.103.80.148')->id,
+ 'net_type' => \App\IP4Net::class
+ ]
+ );
+
+ $setting = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainHosted->id,
+ 'object_type' => \App\Domain::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'true'
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertTrue($request->shouldDefer());
+
+ $connect->created_at = \Carbon\Carbon::now()->subMinutes(6);
+ $connect->save();
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testDomainDisabledUserDisabled()
+ {
+ $connect = \App\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
+ ]
+ );
+
+ $settingDomain = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainHosted->id,
+ 'object_type' => \App\Domain::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'false'
+ ]
+ );
+
+ $settingUser = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainOwner->id,
+ 'object_type' => \App\User::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'false'
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testDomainDisabledUserEnabled()
+ {
+ $connect = \App\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
+ ]
+ );
+
+ $settingDomain = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainHosted->id,
+ 'object_type' => \App\Domain::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'false'
+ ]
+ );
+
+ $settingUser = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainOwner->id,
+ 'object_type' => \App\User::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'true'
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertTrue($request->shouldDefer());
+
+ $connect->created_at = \Carbon\Carbon::now()->subMinutes(6);
+ $connect->save();
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testInvalidDomain()
+ {
+ $connect = \App\Greylist\Connect::create(
+ [
+ 'sender_local' => 'someone',
+ 'sender_domain' => 'sender.domain',
+ 'recipient_hash' => hash('sha256', $this->domainOwner->email),
+ 'recipient_id' => 1234,
+ 'recipient_type' => \App\Domain::class,
+ 'connect_count' => 1,
+ 'net_id' => $this->net->id,
+ 'net_type' => \App\IP4Net::class
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => 'not.someone@that.exists',
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertTrue($request->shouldDefer());
+ }
+
+ public function testInvalidUser()
+ {
+ $connect = \App\Greylist\Connect::create(
+ [
+ 'sender_local' => 'someone',
+ 'sender_domain' => 'sender.domain',
+ 'recipient_hash' => hash('sha256', $this->domainOwner->email),
+ 'recipient_id' => 1234,
+ 'recipient_type' => \App\User::class,
+ 'connect_count' => 1,
+ 'net_id' => $this->net->id,
+ 'net_type' => \App\IP4Net::class
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => 'not.someone@that.exists',
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertTrue($request->shouldDefer());
+ }
+
+ public function testUserDisabled()
+ {
+ $connect = \App\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
+ ]
+ );
+
+ $setting = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainOwner->id,
+ 'object_type' => \App\User::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'false'
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testUserEnabled()
+ {
+ $connect = \App\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
+ ]
+ );
+
+ $setting = \App\Greylist\Setting::create(
+ [
+ 'object_id' => $this->domainOwner->id,
+ 'object_type' => \App\User::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'true'
+ ]
+ );
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertTrue($request->shouldDefer());
+
+ $connect->created_at = \Carbon\Carbon::now()->subMinutes(6);
+ $connect->save();
+
+ $this->assertFalse($request->shouldDefer());
+ }
+
+ public function testMultipleUsersAllDisabled()
+ {
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ foreach ($this->domainUsers as $user) {
+ \App\Greylist\Connect::create(
+ [
+ 'sender_local' => 'someone',
+ 'sender_domain' => 'sender.domain',
+ 'recipient_hash' => hash('sha256', $user->email),
+ 'recipient_id' => $user->id,
+ 'recipient_type' => \App\User::class,
+ 'connect_count' => 1,
+ 'net_id' => $this->net->id,
+ 'net_type' => \App\IP4Net::class
+ ]
+ );
+
+ \App\Greylist\Setting::create(
+ [
+ 'object_id' => $user->id,
+ 'object_type' => \App\User::class,
+ 'key' => 'greylist_enabled',
+ 'value' => 'false'
+ ]
+ );
+
+ if ($user->email == $this->domainOwner->email) {
+ continue;
+ }
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $user->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ $this->assertFalse($request->shouldDefer());
+ }
+ }
+
+ public function testMultipleUsersAnyEnabled()
+ {
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->domainOwner->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ foreach ($this->domainUsers as $user) {
+ \App\Greylist\Connect::create(
+ [
+ 'sender_local' => 'someone',
+ 'sender_domain' => 'sender.domain',
+ 'recipient_hash' => hash('sha256', $user->email),
+ 'recipient_id' => $user->id,
+ 'recipient_type' => \App\User::class,
+ 'connect_count' => 1,
+ 'net_id' => $this->net->id,
+ 'net_type' => \App\IP4Net::class
+ ]
+ );
+
+ \App\Greylist\Setting::create(
+ [
+ 'object_id' => $user->id,
+ 'object_type' => \App\User::class,
+ 'key' => 'greylist_enabled',
+ 'value' => ($user->id == $this->jack->id) ? 'true' : 'false'
+ ]
+ );
+
+ if ($user->email == $this->domainOwner->email) {
+ continue;
+ }
+
+ $request = new \App\Greylist\Request(
+ [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $user->email,
+ 'client_address' => $this->clientAddress
+ ]
+ );
+
+ if ($user->id == $this->jack->id) {
+ $this->assertTrue($request->shouldDefer());
+ } else {
+ $this->assertFalse($request->shouldDefer());
+ }
+ }
+ }
+
+ private function generateInstanceId()
+ {
+ $instance = [];
+
+ for ($x = 0; $x < 3; $x++) {
+ for ($y = 0; $y < 3; $y++) {
+ $instance[] .= substr('01234567889', rand(0, 9), 1);
+ }
+ }
+
+ return implode('.', $instance);
+ }
+}
diff --git a/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Stories/SenderPolicyFrameworkTest.php
@@ -0,0 +1,306 @@
+<?php
+
+namespace Tests\Feature\Stories;
+
+use Tests\TestCase;
+
+class SenderPolicyFrameworkTest extends TestCase
+{
+/*
+ TODO:
+ - with cache,
+ - without cache,
+ - with expired cache?
+*/
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/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->domainOwner->email
+ ];
+
+ $this->assertFalse(strpos(':', $data['client_address']));
+
+ $response = $this->post('/api/webhooks/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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(200);
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(200);
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->setSetting('spf_whitelist', json_encode(['the.only.acceptable.helo']));
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->removeSetting('spf_whitelist');
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->setSetting('spf_whitelist', json_encode(['helo.some.relayservice.domain']));
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(200);
+
+ $this->domainOwner->removeSetting('spf_whitelist');
+ }
+
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->setSetting('spf_whitelist', json_encode(['/a\.domain/']));
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->removeSetting('spf_whitelist');
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->setSetting('spf_whitelist', json_encode(['/relayservice\.domain/']));
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(200);
+
+ $this->domainOwner->removeSetting('spf_whitelist');
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->setSetting('spf_whitelist', json_encode(['.helo.some.relayservice.domain']));
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->removeSetting('spf_whitelist');
+ }
+
+ 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->domainOwner->email
+ ];
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(403);
+
+ $this->domainOwner->setSetting('spf_whitelist', json_encode(['.some.relayservice.domain']));
+
+ $response = $this->post('/api/webhooks/spf', $data);
+
+ $response->assertStatus(200);
+
+ $this->domainOwner->removeSetting('spf_whitelist');
+ }
+}
diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php
--- a/src/tests/TestCase.php
+++ b/src/tests/TestCase.php
@@ -8,19 +8,6 @@
{
use TestCaseTrait;
- protected function backdateEntitlements($entitlements, $targetDate)
- {
- foreach ($entitlements as $entitlement) {
- $entitlement->created_at = $targetDate;
- $entitlement->updated_at = $targetDate;
- $entitlement->save();
-
- $owner = $entitlement->wallet->owner;
- $owner->created_at = $targetDate;
- $owner->save();
- }
- }
-
/**
* Set baseURL to the admin UI location
*/
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -12,6 +12,82 @@
trait TestCaseTrait
{
+ /**
+ * A domain that is hosted.
+ *
+ * @var \App\Domain
+ */
+ protected $domainHosted;
+
+ /**
+ * The hosted domain owner.
+ *
+ * @var \App\User
+ */
+ protected $domainOwner;
+
+ /**
+ * Some profile details for an owner of a domain
+ *
+ * @var array
+ */
+ protected $domainOwnerSettings = [
+ 'first_name' => 'John',
+ 'last_name' => 'Doe',
+ 'organization' => 'Test Domain Owner',
+ ];
+
+ /**
+ * Some users for the hosted domain, ultimately including the owner.
+ *
+ * @var \App\User[]
+ */
+ protected $domainUsers = [];
+
+ /**
+ * A specific user that is a regular user in the hosted domain.
+ */
+ protected $jack;
+
+ /**
+ * A specific user that is a controller on the wallet to which the hosted domain is charged.
+ */
+ protected $jane;
+
+ /**
+ * A specific user that has a second factor configured.
+ */
+ protected $joe;
+
+ /**
+ * One of the domains that is available for public registration.
+ *
+ * @var \App\Domain
+ */
+ protected $publicDomain;
+
+ /**
+ * A newly generated user in a public domain.
+ *
+ * @var \App\User
+ */
+ protected $publicDomainUser;
+
+ /**
+ * A placeholder for a password that can be generated.
+ *
+ * Should be generated with `\App\Utils::generatePassphrase()`.
+ *
+ * @var string
+ */
+ protected $userPassword;
+
+ /**
+ * Assert that the entitlements for the user match the expected list of entitlements.
+ *
+ * @param \App\User $user The user for which the entitlements need to be pulled.
+ * @param array $expected An array of expected \App\SKU titles.
+ */
protected function assertUserEntitlements($user, $expected)
{
// Assert the user entitlements
@@ -26,6 +102,25 @@
Assert::assertSame($expected, $skus);
}
+ /**
+ * Backdate entitlements to the desired target date.
+ *
+ * @param \App\Entitlement[] $entitlements
+ * @param \Carbon\Carbon $targetDate
+ */
+ protected function backdateEntitlements($entitlements, $targetDate)
+ {
+ foreach ($entitlements as $entitlement) {
+ $entitlement->created_at = $targetDate;
+ $entitlement->updated_at = $targetDate;
+ $entitlement->save();
+
+ $owner = $entitlement->wallet->domainOwner;
+ $owner->created_at = $targetDate;
+ $owner->save();
+ }
+ }
+
/**
* Creates the application.
*
@@ -49,6 +144,7 @@
$date = Carbon::now();
$debit = 0;
$entitlementTransactions = [];
+
foreach ($wallet->entitlements as $entitlement) {
if ($entitlement->cost) {
$debit += $entitlement->cost;
@@ -59,28 +155,35 @@
}
}
- $transaction = Transaction::create([
+ $transaction = Transaction::create(
+ [
'user_email' => 'jeroen@jeroen.jeroen',
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => Transaction::WALLET_DEBIT,
'amount' => $debit,
'description' => 'Payment',
- ]);
+ ]
+ );
+
$result[] = $transaction;
Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]);
- $transaction = Transaction::create([
+ $transaction = Transaction::create(
+ [
'user_email' => null,
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => Transaction::WALLET_CREDIT,
'amount' => 2000,
'description' => 'Payment',
- ]);
+ ]
+ );
+
$transaction->created_at = $date->next(Carbon::MONDAY);
$transaction->save();
+
$result[] = $transaction;
$types = [
@@ -91,22 +194,31 @@
// The page size is 10, so we generate so many to have at least two pages
$loops = 10;
while ($loops-- > 0) {
- $transaction = Transaction::create([
- 'user_email' => 'jeroen.@jeroen.jeroen',
- 'object_id' => $wallet->id,
- 'object_type' => \App\Wallet::class,
- 'type' => $types[count($result) % count($types)],
- 'amount' => 11 * (count($result) + 1),
- 'description' => 'TRANS' . $loops,
- ]);
+ $transaction = Transaction::create(
+ [
+ 'user_email' => 'jeroen.@jeroen.jeroen',
+ 'object_id' => $wallet->id,
+ 'object_type' => \App\Wallet::class,
+ 'type' => $types[count($result) % count($types)],
+ 'amount' => 11 * (count($result) + 1),
+ 'description' => 'TRANS' . $loops,
+ ]
+ );
+
$transaction->created_at = $date->next(Carbon::MONDAY);
$transaction->save();
+
$result[] = $transaction;
}
return $result;
}
+ /**
+ * Delete a test domain whatever it takes.
+ *
+ * @coversNothing
+ */
protected function deleteTestDomain($name)
{
Queue::fake();
@@ -123,6 +235,11 @@
$domain->forceDelete();
}
+ /**
+ * Delete a test user whatever it takes.
+ *
+ * @coversNothing
+ */
protected function deleteTestUser($email)
{
Queue::fake();
@@ -139,9 +256,23 @@
$user->forceDelete();
}
+ /**
+ * Helper to access protected property of an object
+ */
+ protected static function getObjectProperty($object, $property_name)
+ {
+ $reflection = new \ReflectionClass($object);
+ $property = $reflection->getProperty($property_name);
+ $property->setAccessible(true);
+
+ return $property->getValue($object);
+ }
+
/**
* Get Domain object by namespace, create it if needed.
* Skip LDAP jobs.
+ *
+ * @coversNothing
*/
protected function getTestDomain($name, $attrib = [])
{
@@ -153,6 +284,8 @@
/**
* Get User object by email, create it if needed.
* Skip LDAP jobs.
+ *
+ * @coversNothing
*/
protected function getTestUser($email, $attrib = [])
{
@@ -171,18 +304,6 @@
return $user;
}
- /**
- * Helper to access protected property of an object
- */
- protected static function getObjectProperty($object, $property_name)
- {
- $reflection = new \ReflectionClass($object);
- $property = $reflection->getProperty($property_name);
- $property->setAccessible(true);
-
- return $property->getValue($object);
- }
-
/**
* Call protected/private method of a class.
*
@@ -200,4 +321,90 @@
return $method->invokeArgs($object, $parameters);
}
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->userPassword = \App\Utils::generatePassphrase();
+
+ $this->domainHosted = $this->getTestDomain(
+ 'test.domain',
+ [
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ 'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED
+ ]
+ );
+
+ $packageKolab = \App\Package::where('title', 'kolab')->first();
+
+ $this->domainOwner = $this->getTestUser('john@test.domain', ['password' => $this->userPassword]);
+ $this->domainOwner->assignPackage($packageKolab);
+ $this->domainOwner->setSettings($this->domainOwnerSettings);
+
+ // separate for regular user
+ $this->jack = $this->getTestUser('jack@test.domain', ['password' => $this->userPassword]);
+
+ // separate for wallet controller
+ $this->jane = $this->getTestUser('jane@test.domain', ['password' => $this->userPassword]);
+
+ $this->joe = $this->getTestUser('joe@test.domain', ['password' => $this->userPassword]);
+
+ $this->domainUsers[] = $this->jack;
+ $this->domainUsers[] = $this->jane;
+ $this->domainUsers[] = $this->joe;
+ $this->domainUsers[] = $this->getTestUser('jill@test.domain', ['password' => $this->userPassword]);
+
+ foreach ($this->domainUsers as $user) {
+ $this->domainOwner->assignPackage($packageKolab, $user);
+ }
+
+ $this->domainUsers[] = $this->domainOwner;
+
+ // assign second factor to joe
+ $this->joe->assignSku(\App\Sku::where('title', '2fa')->first());
+ \App\Auth\SecondFactor::seed($this->joe->email);
+
+ usort(
+ $this->domainUsers,
+ function ($a, $b) {
+ return $a->email > $b->email;
+ }
+ );
+
+ $this->domainHosted->assignPackage(
+ \App\Package::where('title', 'domain-hosting')->first(),
+ $this->domainOwner
+ );
+
+ $wallet = $this->domainOwner->wallets()->first();
+
+ $wallet->addController($this->jane);
+
+ $this->publicDomain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first();
+ $this->publicDomainUser = $this->getTestUser(
+ 'john@' . $this->publicDomain->namespace,
+ ['password' => $this->userPassword]
+ );
+
+ $this->publicDomainUser->assignPackage($packageKolab);
+ }
+
+ public function tearDown(): void
+ {
+ foreach ($this->domainUsers as $user) {
+ if ($user == $this->domainOwner) {
+ continue;
+ }
+
+ $this->deleteTestUser($user->email);
+ }
+
+ $this->deleteTestUser($this->domainOwner->email);
+ $this->deleteTestDomain($this->domainHosted->namespace);
+
+ $this->deleteTestUser($this->publicDomainUser->email);
+
+ parent::tearDown();
+ }
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 2, 6:08 PM (5 m, 16 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820203
Default Alt Text
D1810.1775153280.diff (79 KB)
Attached To
Mode
D1810: Preliminary implementation of a greylisting database and spf service
Attached
Detach File
Event Timeline