Page MenuHomePhorge

D5559.1775242569.diff
No OneTemporary

Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None

D5559.1775242569.diff

diff --git a/docker/postfix/rootfs/etc/postfix/main.cf b/docker/postfix/rootfs/etc/postfix/main.cf
--- a/docker/postfix/rootfs/etc/postfix/main.cf
+++ b/docker/postfix/rootfs/etc/postfix/main.cf
@@ -496,11 +496,24 @@
# plaintext (opportunistic TLS outbound).
#
smtp_tls_security_level = may
+
meta_directory = /etc/postfix
shlib_directory = /usr/lib64/postfix
recipient_delimiter = +
-transport_maps = regexp:/etc/postfix/transport
smtpd_tls_auth_only = no
+smtpd_helo_required = yes
+smtpd_peername_lookup = yes
+smtpd_sasl_auth_enable = yes
+maillog_file = /dev/stdout
+message_size_limit = MESSAGE_SIZE_LIMIT
+
+# Disable BDAT support without the useless "discarding EHLO keywords: CHUNKING" message
+smtpd_discard_ehlo_keywords = chunking, silent-discard
+
+content_filter = smtp-amavis:[AMAVIS_HOST]:13024
+transport_maps = regexp:/etc/postfix/transport
+
+smtpd_sender_login_maps = mysql:/etc/postfix/sql/local_recipient_maps.cf
local_recipient_maps =
mysql:/etc/postfix/sql/local_recipient_maps.cf,
@@ -513,43 +526,17 @@
mysql:/etc/postfix/sql/virtual_alias_maps_groups.cf,
mysql:/etc/postfix/sql/virtual_alias_maps_shared_folders.cf
-# Inbound
+# Inbound (restrictions in order of execution)
smtpd_client_restrictions =
permit_mynetworks,
reject_unknown_reverse_client_hostname,
#reject_rbl_client zen.spamhaus.org,
#reject_rhsbl_reverse_client dbl.spamhaus.org
-smtpd_data_restrictions =
- reject_unauth_pipelining
-smtpd_helo_required = yes
smtpd_helo_restrictions =
permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
# reject_rhsbl_helo dbl.spamhaus.org
-# Local clients and authenticated clients may specify any destination domain
-smtpd_relay_restrictions =
- permit_mynetworks,
- permit_sasl_authenticated,
- reject_unauth_destination
-smtpd_recipient_restrictions =
- permit_mynetworks,
- reject_invalid_hostname,
- reject_non_fqdn_sender,
- reject_non_fqdn_recipient,
- reject_unauth_destination,
- #reject_rhsbl_recipient dbl.spamhaus.org,
- # TODO block suspended recipients (by domain or account)
- #check_recipient_access ldap:/etc/postfix/ldap/domain_suspended.cf,
- #check_recipient_access ldap:/etc/postfix/ldap/account_suspended.cf,
- check_policy_service unix:private/policy_greylist,
- permit
-smtpd_peername_lookup = yes
-smtpd_sasl_auth_enable = yes
-
-smtpd_sender_login_maps =
- mysql:/etc/postfix/sql/local_recipient_maps.cf
-
smtpd_sender_restrictions =
# We used to also block spammers via sender_access
check_sender_access hash:/etc/postfix/sender_access,
@@ -564,7 +551,21 @@
check_policy_service unix:private/policy_spf,
#reject_rhsbl_sender dbl.spamhaus.org,
permit
-
+# Local clients and authenticated clients may specify any destination domain
+smtpd_relay_restrictions =
+ permit_mynetworks,
+ permit_sasl_authenticated,
+ reject_unauth_destination
+smtpd_recipient_restrictions =
+ permit_mynetworks,
+ reject_invalid_hostname,
+ reject_non_fqdn_sender,
+ reject_non_fqdn_recipient,
+ reject_unauth_destination,
+ #reject_rhsbl_recipient dbl.spamhaus.org,
+ check_policy_service unix:private/policy_reception,
+ permit
+smtpd_data_restrictions = reject_unauth_pipelining
# Outbound
submission_data_restrictions =
@@ -601,12 +602,3 @@
permit_sasl_authenticated,
# permit_mynetworks,
reject
-
-# Disable BDAT support without the useless "discarding EHLO keywords: CHUNKING" message
-smtpd_discard_ehlo_keywords = chunking, silent-discard
-
-content_filter = smtp-amavis:[AMAVIS_HOST]:13024
-
-maillog_file = /dev/stdout
-
-message_size_limit = MESSAGE_SIZE_LIMIT
diff --git a/docker/postfix/rootfs/etc/postfix/master.cf b/docker/postfix/rootfs/etc/postfix/master.cf
--- a/docker/postfix/rootfs/etc/postfix/master.cf
+++ b/docker/postfix/rootfs/etc/postfix/master.cf
@@ -127,8 +127,12 @@
user=nobody argv=/usr/libexec/postfix/kolab_policy_submission
# Inbound
-policy_greylist unix - n n - - spawn
- user=nobody argv=/usr/libexec/postfix/kolab_policy greylist /api/webhooks/policy/greylist
+policy_reception unix - n n - - spawn
+ user=nobody argv=/usr/libexec/postfix/kolab_policy reception /api/webhooks/policy/reception
+
+# Inbound
+#policy_greylist unix - n n - - spawn
+# user=nobody argv=/usr/libexec/postfix/kolab_policy greylist /api/webhooks/policy/greylist
# Inbound
policy_spf unix - n n - - spawn
diff --git a/docker/postfix/rootfs/init.sh b/docker/postfix/rootfs/init.sh
--- a/docker/postfix/rootfs/init.sh
+++ b/docker/postfix/rootfs/init.sh
@@ -18,10 +18,11 @@
/etc/postfix/main.cf
mkdir /var/log/kolab
+touch /var/log/kolab/postfix-content-filter.log
+#touch /var/log/kolab/postfix-policy-greylist.log
touch /var/log/kolab/postfix-policy-submission.log
+touch /var/log/kolab/postfix-policy-reception.log
touch /var/log/kolab/postfix-policy-spf.log
-touch /var/log/kolab/postfix-policy-greylist.log
-touch /var/log/kolab/postfix-content-filter.log
chmod -R 777 /var/log/kolab
chown -R postfix:mail /var/lib/postfix
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
@@ -124,6 +124,18 @@
return $response->jsonResponse();
}
+ /**
+ * Validate a mail reception request (includes greylisting)
+ *
+ * @return JsonResponse
+ */
+ public function reception()
+ {
+ $response = SmtpAccess::reception(\request()->input());
+
+ return $response->jsonResponse();
+ }
+
/*
* Apply the sender policy framework to a request.
*
diff --git a/src/app/Policy/SmtpAccess.php b/src/app/Policy/SmtpAccess.php
--- a/src/app/Policy/SmtpAccess.php
+++ b/src/app/Policy/SmtpAccess.php
@@ -2,12 +2,31 @@
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'])) {
+ return new Response(Response::ACTION_REJECT, 'Invalid recipient', 403);
+ }
+
+ // Greylisting
+ $response = Greylist::handle($data);
+
+ return $response;
+ }
+
/**
* Handle SMTP submission request
*
@@ -113,4 +132,56 @@
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) {
+ // 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/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -315,6 +315,7 @@
Route::post('policy/greylist', [API\V4\PolicyController::class, 'greylist']);
Route::post('policy/ratelimit', [API\V4\PolicyController::class, 'ratelimit']);
Route::post('policy/spf', [API\V4\PolicyController::class, 'senderPolicyFramework']);
+ Route::post('policy/reception', [API\V4\PolicyController::class, 'reception']);
Route::post('policy/submission', [API\V4\PolicyController::class, 'submission']);
Route::post('policy/mail/filter', [API\V4\PolicyController::class, 'mailfilter']);
}
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
@@ -5,6 +5,7 @@
use App\Domain;
use App\IP4Net;
use App\Policy\Greylist;
+use App\Policy\Mailfilter;
use Carbon\Carbon;
use Tests\TestCase;
@@ -45,6 +46,7 @@
protected function tearDown(): void
{
+ $this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestUser($this->testUser->email);
$this->deleteTestDomain($this->testDomain->namespace);
$this->net->delete();
@@ -269,7 +271,8 @@
// Basic test, no changes to the mail content
$url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org&sender=jack@kolab.org';
$response = $this->call('POST', $url, [], [], [], $headers, $post)
- ->assertNoContent(204);
+ ->assertNoContent(200)
+ ->assertHeader(Mailfilter::HEADER, Mailfilter::HEADER_ACTION_ACCEPT_EMPTY);
// Test returning (modified) mail content
$john->setConfig(['externalsender_policy' => true, 'itip_policy' => true]);
@@ -282,6 +285,57 @@
$this->assertStringContainsString('Subject: [EXTERNAL] test sync', $content);
}
+ /**
+ * Test mail reception policy webhook
+ */
+ public function testReception()
+ {
+ // Note: Only basic tests here. More detailed policy handler tests are in another place
+
+ // Test 403 response
+ $post = [
+ 'sender' => 'someone@sender.domain',
+ 'recipient' => $this->testUser->email,
+ 'client_address' => $this->clientAddress,
+ 'client_name' => 'some.mx',
+ ];
+
+ $response = $this->post('/api/webhooks/policy/reception', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertSame('DEFER_IF_PERMIT', $json['response']);
+ $this->assertSame("Greylisted for 5 minutes. Try again later.", $json['reason']);
+
+ // Test 200 response
+ $connect = Greylist\Connect::where('sender_domain', 'sender.domain')->first();
+ $connect->created_at = Carbon::now()->subMinutes(6);
+ $connect->save();
+
+ $response = $this->post('/api/webhooks/policy/reception', $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('DUNNO', $json['response']);
+ $this->assertMatchesRegularExpression('/^Received-Greylist: greylisted from/', $json['prepend'][0]);
+
+ // Test sender access check (403)
+ $group = $this->getTestGroup('group-test@kolab.org');
+ $group->setConfig(['sender_policy' => ['aaa.pl']]);
+
+ $post['recipient'] = $group->email;
+ $response = $this->post('/api/webhooks/policy/reception', $post);
+ $response->assertStatus(403);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('REJECT', $json['response']);
+ $this->assertSame('Invalid recipient', $json['reason']);
+ }
+
/**
* Test submission policy webhook
*/
diff --git a/src/tests/Feature/Policy/SmtpAccessTest.php b/src/tests/Feature/Policy/SmtpAccessTest.php
--- a/src/tests/Feature/Policy/SmtpAccessTest.php
+++ b/src/tests/Feature/Policy/SmtpAccessTest.php
@@ -25,6 +25,7 @@
protected function tearDown(): void
{
+ $this->deleteTestGroup('group-test@kolab.org');
Delegation::query()->delete();
$john = $this->getTestUser('john@kolab.org');
$john->status &= ~User::STATUS_SUSPENDED;
@@ -36,6 +37,37 @@
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));
+
+ $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));
+ }
+
/**
* Test verifySender() method
*/

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 6:56 PM (4 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18778638
Default Alt Text
D5559.1775242569.diff (13 KB)

Event Timeline