Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117916054
D5010.1775408878.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
D5010.1775408878.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D5010: External sender module
Attached
Detach File
Event Timeline