Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117758278
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Policy/SmtpAccess.php b/src/app/Policy/SmtpAccess.php
index 3b9f36e4..a7bd6d08 100644
--- a/src/app/Policy/SmtpAccess.php
+++ b/src/app/Policy/SmtpAccess.php
@@ -1,187 +1,187 @@
<?php
namespace App\Policy;
use App\Group;
use App\User;
use App\UserAlias;
use App\Utils;
class SmtpAccess
{
/**
* Handle SMTP external mail reception request
*
* @param array $data Input data
*/
public static function reception($data): Response
{
// Check access policy
- if (!self::verifyRecipient($data['sender'], $data['recipient'])) {
+ if (!self::verifyRecipient($data['sender'] ?? '', $data['recipient'])) {
return new Response(Response::ACTION_REJECT, 'Invalid recipient', 403);
}
// Greylisting
$response = Greylist::handle($data);
return $response;
}
/**
* Handle SMTP submission request
*
* @param array $data Input data
*/
public static function submission($data): Response
{
// TODO: The old SMTP access policy had an option ('empty_sender_hosts') to allow
// sending mail with no sender from configured networks.
[$local, $domain] = Utils::normalizeAddress($data['sender'], true);
if (empty($local) || empty($domain)) {
return new Response(Response::ACTION_REJECT, 'Invalid sender', 403);
}
$sender = $local . '@' . $domain;
[$local, $domain] = Utils::normalizeAddress($data['user'], true);
if (empty($local) || empty($domain)) {
return new Response(Response::ACTION_REJECT, 'Invalid user', 403);
}
$sasl_user = $local . '@' . $domain;
$user = User::where('email', $sasl_user)->first();
if (!$user) {
return new Response(Response::ACTION_REJECT, "Could not find user {$data['user']}", 403);
}
if (!self::verifySender($user, $sender)) {
$reason = "{$sasl_user} is unauthorized to send mail as {$sender}";
return new Response(Response::ACTION_REJECT, $reason, 403);
}
// TODO: should we be using the $user or the $sender?
$response = RateLimit::verifyRequest($user, (array) $data['recipients']);
if ($response->action != Response::ACTION_DUNNO) {
return $response;
}
// TODO: Prepending Sender/X-Sender/X-Authenticated-As headers?
// Leave it up to the postfix configuration how to proceed (accept would stop processing)
return new Response(Response::ACTION_DUNNO);
}
/**
* Verify whether a user is allowed to send using the envelope sender address.
*
* @param User $user Authenticated user
* @param string $email Email address
*/
public static function verifySender(User $user, string $email): bool
{
if ($user->isSuspended() || !str_contains($email, '@')) {
return false;
}
// TODO: Make sure the domain is not suspended
$email = \strtolower($email);
if ($user->email == $email) {
return true;
}
// noreply@ user can impersonate everyone
if ($user->email == \config('mail.mailers.smtp.username')) {
return true;
}
// Is it one of user's aliases?
$alias = $user->aliases()->where('alias', $email)->first();
if ($alias) {
return true;
}
// Delegation
if (\config('app.with_delegation')) {
// Is it another user's email?
$other_users = User::where('email', $email)->pluck('id')->all();
if (!count($other_users)) {
// Is it another user's alias?
$other_users = UserAlias::where('alias', $email)->pluck('user_id')->all();
}
if (count($other_users)) {
// Is the user a delegatee of that other user? Is he suspended?
$is_delegate = $user->delegators()->whereIn('user_id', $other_users)
->whereNot('users.status', '&', User::STATUS_SUSPENDED)
->exists();
if ($is_delegate) {
return true;
}
}
}
return false;
}
/**
* Verify whether a sender is allowed to send mail to the recipient address.
*
* @param string $sender Sender email address
* @param string $recipient Recipient email address
*/
public static function verifyRecipient(string $sender, string $recipient): bool
{
$sender = \strtolower($sender);
- if (!str_contains($sender, '@')) {
- return false;
- }
-
$group = Group::where('email', $recipient)->first();
// Check distribution list sender access list
if ($group) {
$policy = $group->getConfig()['sender_policy'];
if (!empty($policy)) {
foreach ($policy as $entry) {
+ // $sender can be empty in case of an empty SMTP FROM
+ if (!str_contains($sender, '@')) {
+ break;
+ }
// Full email address match
if (str_contains($entry, '@')) {
if ($sender === $entry) {
return true;
}
} else {
[$local, $domain] = explode('@', $sender);
// Domain suffix match
if (str_starts_with($entry, '.')) {
if (str_ends_with($domain, $entry)) {
return true;
}
}
// Full domain match
elseif ($entry === $domain) {
return true;
}
}
}
return false;
}
}
// TODO: Check domain/recipient suspended status?
return true;
}
}
diff --git a/src/tests/Feature/Policy/SmtpAccessTest.php b/src/tests/Feature/Policy/SmtpAccessTest.php
index f8d75357..2497629b 100644
--- a/src/tests/Feature/Policy/SmtpAccessTest.php
+++ b/src/tests/Feature/Policy/SmtpAccessTest.php
@@ -1,118 +1,129 @@
<?php
namespace Tests\Feature\Policy;
use App\Delegation;
use App\Policy\SmtpAccess;
use App\User;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class SmtpAccessTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Delegation::query()->delete();
$john = $this->getTestUser('john@kolab.org');
$john->status &= ~User::STATUS_SUSPENDED;
$john->save();
$jack = $this->getTestUser('jack@kolab.org');
$jack->status &= ~User::STATUS_SUSPENDED;
$jack->save();
}
protected function tearDown(): void
{
$this->deleteTestGroup('group-test@kolab.org');
Delegation::query()->delete();
$john = $this->getTestUser('john@kolab.org');
$john->status &= ~User::STATUS_SUSPENDED;
$john->save();
$jack = $this->getTestUser('jack@kolab.org');
$jack->status &= ~User::STATUS_SUSPENDED;
$jack->save();
parent::tearDown();
}
/**
* Test verifyRecipient() method
*/
public function testVerifyRecipient(): void
{
$group = $this->getTestGroup('group-test@kolab.org');
- // invalid sender address
- $this->assertFalse(SmtpAccess::verifyRecipient('invalid', 'none@unknown.tld'));
-
// non-existing recipient
$this->assertTrue(SmtpAccess::verifyRecipient('ext@gmail.com', 'none@unknown.tld'));
// no policy for a group
$this->assertTrue(SmtpAccess::verifyRecipient('ext@gmail.com', $group->email));
+ // empty sender
+ $this->assertTrue(SmtpAccess::verifyRecipient('', $group->email));
+
$group->setConfig(['sender_policy' => ['.gmail.com', 'allowed.tld', 'allowed@kolab.org']]);
// domain suffix match
$this->assertTrue(SmtpAccess::verifyRecipient('ext@test.gmail.com', $group->email));
// domain match
$this->assertTrue(SmtpAccess::verifyRecipient('ext@allowed.tld', $group->email));
// email address match
$this->assertTrue(SmtpAccess::verifyRecipient('allowed@kolab.org', $group->email));
// no match
$this->assertFalse(SmtpAccess::verifyRecipient('test@kolab.ch', $group->email));
+
+ // empty sender
+ $this->assertFalse(SmtpAccess::verifyRecipient('', $group->email));
+
+ // User recipient
+ $this->assertTrue(SmtpAccess::verifyRecipient('anyone@gmail.com', 'john@kolab.org'));
+ $this->assertTrue(SmtpAccess::verifyRecipient('', 'john@kolab.org'));
+
+ // Non-existing recipient (?)
+ $this->assertTrue(SmtpAccess::verifyRecipient('anyone@gmail.com', 'unknown@unknown.org'));
+ $this->assertTrue(SmtpAccess::verifyRecipient('', 'unknown@unknown.org'));
}
/**
* Test verifySender() method
*/
public function testVerifySender(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$noreply = User::where('email', \config('mail.mailers.smtp.username'))->first();
// Test main email address
$this->assertTrue(SmtpAccess::verifySender($john, ucfirst($john->email)));
// Test noreply@ user
if ($noreply) {
$this->assertTrue(SmtpAccess::verifySender($noreply, $john->email));
}
// Test an alias
$this->assertTrue(SmtpAccess::verifySender($john, 'John.Doe@kolab.org'));
// Test another user's email address
$this->assertFalse(SmtpAccess::verifySender($jack, $john->email));
// Test another user's alias
$this->assertFalse(SmtpAccess::verifySender($jack, 'john.doe@kolab.org'));
Queue::fake();
Delegation::create(['user_id' => $john->id, 'delegatee_id' => $jack->id]);
// Test delegator's email address
$this->assertTrue(SmtpAccess::verifySender($jack, $john->email));
// Test delegator's alias
$this->assertTrue(SmtpAccess::verifySender($jack, 'john.doe@kolab.org'));
// Test delegator's alias, but suspended delegator
$john->suspend();
$this->assertFalse(SmtpAccess::verifySender($jack, 'john.doe@kolab.org'));
// Test invalid/unknown email
$this->assertFalse(SmtpAccess::verifySender($jack, 'unknown'));
$this->assertFalse(SmtpAccess::verifySender($jack, 'unknown@domain.tld'));
// Test suspended user
$jack->suspend();
$this->assertFalse(SmtpAccess::verifySender($jack, $jack->email));
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Apr 4, 9:41 AM (3 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823512
Default Alt Text
(10 KB)
Attached To
Mode
rK kolab
Attached
Detach File
Event Timeline