Page MenuHomePhorge

D5010.1775408878.diff
No OneTemporary

Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None

D5010.1775408878.diff

diff --git a/src/app/Policy/Mailfilter/MailParser.php b/src/app/Policy/Mailfilter/MailParser.php
--- a/src/app/Policy/Mailfilter/MailParser.php
+++ b/src/app/Policy/Mailfilter/MailParser.php
@@ -24,6 +24,7 @@
'content-transfer-encoding',
'content-type',
'from',
+ 'subject',
];
/**
@@ -223,6 +224,8 @@
*
* @param string $body Body content
* @param ?int $part_id Part identifier (NULL to replace the whole message body)
+ *
+ * @throws \Exception
*/
public function replaceBody($body, $part_id = null): void
{
@@ -264,6 +267,69 @@
$this->modified = true;
}
+ /**
+ * Set header value
+ *
+ * @param string $header Header name
+ * @param ?string $value Header value
+ *
+ * @throws \Exception
+ */
+ public function setHeader(string $header, ?string $value = null): void
+ {
+ // TODO: This method should work also on parts, but we'd have to reset all parents
+ if ($this->start > 0) {
+ throw new \Exception("Setting header supported on the message level only");
+ }
+
+ $header_name = strtolower($header);
+ $header_name_len = strlen($header);
+
+ // Create a new resource stream to copy the content into
+ $copy = fopen('php://temp', 'r+');
+
+ // Insert the new header on top
+ if (is_string($value)) {
+ fwrite($copy, "{$header}: {$value}\r\n");
+ $this->headers[$header_name] = $value;
+ } else {
+ unset($this->headers[$header_name]);
+ }
+
+ fseek($this->stream, $position = $this->start);
+
+ // Go throughout all headers and remove the one
+ $found = false;
+ while (($line = fgets($this->stream, 2048)) !== false) {
+ if ($line == "\n" || $line == "\r\n") {
+ break;
+ }
+
+ if ($line[0] == ' ' || $line[0] == "\t") {
+ if (!$found) {
+ fwrite($copy, $line);
+ }
+ } elseif (strtolower(substr($line, 0, $header_name_len + 1)) == "{$header_name}:") {
+ $found = true;
+ } else {
+ fwrite($copy, $line);
+ $found = false;
+ }
+
+ $position += strlen($line);
+ }
+
+ // Copy the rest of the message
+ stream_copy_to_stream($this->stream, $copy, null, $position);
+
+ $this->stream = $copy;
+ $this->bodyPosition = $position + 2;
+
+ // Reset structure information, the message will need to be re-parsed (in some cases)
+ $this->parts = null;
+ $this->modified = true;
+ }
+
/**
* Set email address of the recipient
*/
diff --git a/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php b/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php
new file mode 100644
--- /dev/null
+++ b/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Policy\Mailfilter\Modules;
+
+use App\Policy\Mailfilter\MailParser;
+use App\Policy\Mailfilter\Result;
+
+class ExternalSenderModule
+{
+ /**
+ * Handle the email message
+ */
+ public function handle(MailParser $parser): ?Result
+ {
+ $sender = $parser->getSender();
+
+ $user = $parser->getUser();
+
+ [, $sender_domain] = explode('@', $sender);
+ [, $user_domain] = explode('@', $user->email);
+
+ $sender_domain = strtolower($sender_domain);
+
+ // Sender and recipient in the same domain
+ if ($sender_domain === $user_domain) {
+ return null; // just accept the message as-is
+ }
+
+ $account = $user->wallet()->owner;
+
+ // Check against the account domains list
+ // TODO: Use a per-account/per-user list of additional domains
+ if ($account->domains(false, false)->where('namespace', $sender_domain)->exists()) {
+ return null; // just accept the message as-is
+ }
+
+ $subject = $parser->getHeader('subject');
+
+ // Update the subject with a prefix
+ if (is_string($subject)) {
+ $subject = '[EXTERNAL] ' . $subject;
+ } else {
+ $subject = '[EXTERNAL]';
+ }
+
+ $parser->setHeader('Subject', $subject);
+
+ return null;
+ }
+}
diff --git a/src/app/Policy/Mailfilter/RequestHandler.php b/src/app/Policy/Mailfilter/RequestHandler.php
--- a/src/app/Policy/Mailfilter/RequestHandler.php
+++ b/src/app/Policy/Mailfilter/RequestHandler.php
@@ -60,11 +60,13 @@
}
// TODO: The list of modules and their config will come from somewhere
- $modules = ['Itip' /*, 'Footer'*/];
+ $modules = [
+ 'itip' => Modules\ItipModule::class,
+ 'external-sender' => Modules\ExternalSenderModule::class,
+ ];
foreach ($modules as $module) {
- $class = "\\App\\Policy\\Mailfilter\\Modules\\{$module}Module";
- $engine = new $class();
+ $engine = new $module();
$result = $engine->handle($parser);
@@ -91,6 +93,7 @@
$stream = $parser->getStream();
$response->setCallback(function () use ($stream) {
+
fpassthru($stream);
fclose($stream);
});
@@ -98,6 +101,6 @@
return $response;
}
- return response('', 201);
+ return response('', 204);
}
}
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
@@ -106,14 +106,23 @@
$post = file_get_contents(__DIR__ . '/../../data/mail/1.eml');
$post = str_replace("\n", "\r\n", $post);
- $url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org';
+ // 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)
- ->assertStatus(201);
+ ->assertNoContent(204);
+
+ // Test returning (modified) mail content
+ $url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org&sender=jack@external.tld';
+ $content = $this->call('POST', $url, [], [], [], $headers, $post)
+ ->assertStatus(200)
+ ->assertHeader('Content-Type', 'message/rfc822')
+ ->streamedContent();
+
+ $this->assertStringContainsString('Subject: [EXTERNAL] test sync', $content);
// TODO: Test multipart/form-data request
- // TODO: test returning (modified) mail content
- // TODO: test rejecting mail
- // TODO: Test running multiple modules
+ // TODO: Test rejecting mail
+ // TODO: Test two modules that both modify the mail content
$this->markTestIncomplete();
}
}
diff --git a/src/tests/Feature/Policy/Mailfilter/Modules/ExternalSenderModuleTest.php b/src/tests/Feature/Policy/Mailfilter/Modules/ExternalSenderModuleTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Policy/Mailfilter/Modules/ExternalSenderModuleTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Tests\Feature\Policy\Mailfilter\Modules;
+
+use App\Policy\Mailfilter\Modules\ExternalSenderModule;
+use App\Policy\Mailfilter\Result;
+use Tests\TestCase;
+use Tests\Unit\Policy\Mailfilter\MailParserTest;
+
+class ExternalSenderModuleTest extends TestCase
+{
+ /**
+ * Test the module
+ */
+ public function testHandle(): void
+ {
+ $domain = \config('app.domain');
+
+ // Test an email from an external sender
+ $parser = MailParserTest::getParserForFile('mail/1.eml', 'john@kolab.org', 'external@sender.tld');
+ $module = new ExternalSenderModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertSame('[EXTERNAL] test sync', $parser->getHeader('subject'));
+
+ // Test an email from an external sender (public domain)
+ $parser = MailParserTest::getParserForFile('mail/1.eml', 'john@kolab.org', "joe@{$domain}");
+ $module = new ExternalSenderModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertSame('[EXTERNAL] test sync', $parser->getHeader('subject'));
+
+ // Test an email from an internal sender (same domain)
+ $parser = MailParserTest::getParserForFile('mail/1.eml', 'john@kolab.org', 'jack@kolab.org');
+ $module = new ExternalSenderModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertSame('test sync', $parser->getHeader('subject'));
+
+ // Test an email from an internal sender (public domain)
+ $parser = MailParserTest::getParserForFile('mail/1.eml', "fred@{$domain}", "joe@{$domain}");
+ $module = new ExternalSenderModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertSame('test sync', $parser->getHeader('subject'));
+
+ // TODO: Test other account domains
+ }
+}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -481,16 +481,17 @@
]);
$domains = $user->domains()->pluck('namespace')->all();
-
$this->assertContains($domain->namespace, $domains);
$this->assertContains('kolab.org', $domains);
+ $domains = $user->domains(false, false)->pluck('namespace')->all();
+ $this->assertSame(['kolab.org'], $domains);
+
// Jack is not the wallet controller, so for him the list should not
// include John's domains, kolab.org specifically
$user = $this->getTestUser('jack@kolab.org');
$domains = $user->domains()->pluck('namespace')->all();
-
$this->assertContains($domain->namespace, $domains);
$this->assertNotContains('kolab.org', $domains);
@@ -500,8 +501,16 @@
$domain->save();
$domains = $user->domains()->pluck('namespace')->all();
-
$this->assertNotContains($domain->namespace, $domains);
+
+ // An account in a public domain
+ $user = $this->getTestUser('user-test@' . \config('app.domain'));
+
+ $domains = $user->domains()->pluck('namespace')->all();
+ $this->assertContains(\config('app.domain'), $domains);
+
+ $domains = $user->domains(true, false)->pluck('namespace')->all();
+ $this->assertSame([], $domains);
}
/**
diff --git a/src/tests/Unit/Policy/Mailfilter/MailParserTest.php b/src/tests/Unit/Policy/Mailfilter/MailParserTest.php
--- a/src/tests/Unit/Policy/Mailfilter/MailParserTest.php
+++ b/src/tests/Unit/Policy/Mailfilter/MailParserTest.php
@@ -20,7 +20,7 @@
$this->assertSame('eeea', $body);
// Multipart/alternative mail
- $parser = $this->getParserForFile('mailfilter/itip1.eml');
+ $parser = $this->getParserForFile('mailfilter/itip1_request.eml');
$body = $parser->getBody();
@@ -47,7 +47,7 @@
public function testGetHeader(): void
{
// Multipart/alternative email
- $parser = $this->getParserForFile('mailfilter/itip1.eml');
+ $parser = $this->getParserForFile('mailfilter/itip1_request.eml');
$this->assertSame('Jack <jack@kolab.org>', $parser->getHeader('from'));
$this->assertSame('Jack <jack@kolab.org>', $parser->getHeader('From'));
@@ -86,7 +86,7 @@
// Replace text part in multipart/alternative mail
// Note: The body is quoted-printable encoded
- $parser = $this->getParserForFile('mailfilter/itip1.eml');
+ $parser = $this->getParserForFile('mailfilter/itip1_request.eml');
$parser->replaceBody('aa=aa', 0);
$part = $parser->getParts()[0];
@@ -106,10 +106,44 @@
$this->assertSame('quoted-printable', $part->getHeader('content-transfer-encoding'));
}
+ /**
+ * Test setHeader()
+ */
+ public function testSetHeader(): void
+ {
+ // Test changing a header
+ $parser = self::getParserForFile('mail/1.eml');
+
+ $this->assertSame('test sync', $parser->getHeader('subject'));
+ $this->assertSame('eeea', $parser->getBody());
+ $this->assertSame('"Sync 1" <test@kolab.org>', $parser->getHeader('from'));
+
+ $parser->setHeader('Subject', 'new subject');
+
+ $this->assertTrue($parser->isModified());
+
+ $parser = new MailParser($parser->getStream());
+
+ $this->assertSame('new subject', $parser->getHeader('subject'));
+ $this->assertSame('eeea', $parser->getBody());
+ $this->assertSame('"Sync 1" <test@kolab.org>', $parser->getHeader('from'));
+
+ // Test removing a header
+ $parser->setHeader('Subject', null);
+
+ $this->assertTrue($parser->isModified());
+
+ $parser = new MailParser($parser->getStream());
+
+ $this->assertSame(null, $parser->getHeader('subject'));
+ $this->assertSame('eeea', $parser->getBody());
+ $this->assertSame('"Sync 1" <test@kolab.org>', $parser->getHeader('from'));
+ }
+
/**
* Create mail parser instance for specified test message
*/
- public static function getParserForFile(string $file, $recipient = null): MailParser
+ public static function getParserForFile(string $file, $recipient = null, $sender = null): MailParser
{
$mail = file_get_contents(__DIR__ . '/../../../data/' . $file);
$mail = str_replace("\n", "\r\n", $mail);
@@ -123,6 +157,9 @@
if ($recipient) {
$parser->setRecipient($recipient);
}
+ if ($sender) {
+ $parser->setSender($sender);
+ }
return $parser;
}

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 5:07 PM (4 h, 58 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833983
Default Alt Text
D5010.1775408878.diff (13 KB)

Event Timeline