Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline