Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117775955
D5559.1775242569.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None
D5559.1775242569.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D5559: Sender access policy for distlist mail delivery
Attached
Detach File
Event Timeline