Page MenuHomePhorge

D5262.1775323559.diff
No OneTemporary

Authored By
Unknown
Size
39 KB
Referenced Files
None
Subscribers
None

D5262.1775323559.diff

diff --git a/src/app/Http/Controllers/API/PasswordPolicyController.php b/src/app/Http/Controllers/API/PasswordPolicyController.php
--- a/src/app/Http/Controllers/API/PasswordPolicyController.php
+++ b/src/app/Http/Controllers/API/PasswordPolicyController.php
@@ -10,33 +10,6 @@
class PasswordPolicyController extends Controller
{
- /**
- * Fetch the password policy for the current user account.
- * The result includes all supported policy rules.
- *
- * @return JsonResponse
- */
- public function index(Request $request)
- {
- // Get the account owner
- $owner = $this->guard()->user()->walletOwner();
-
- // Get the policy
- $policy = new Password($owner);
- $rules = $policy->rules(true);
-
- // Get the account's password retention config
- $config = [
- 'max_password_age' => $owner->getSetting('max_password_age'),
- ];
-
- return response()->json([
- 'list' => array_values($rules),
- 'count' => count($rules),
- 'config' => $config,
- ]);
- }
-
/**
* Validate the password regarding the defined policies.
*
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
@@ -8,6 +8,7 @@
use App\Policy\RateLimit;
use App\Policy\SmtpAccess;
use App\Policy\SPF;
+use App\Rules\Password;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -26,6 +27,50 @@
return $response->jsonResponse();
}
+ /**
+ * Fetch the account policies for the current user account.
+ * The result includes all supported policy rules.
+ *
+ * @return JsonResponse
+ */
+ public function index(Request $request)
+ {
+ $user = $this->guard()->user();
+
+ if (!$this->checkTenant($user)) {
+ return $this->errorResponse(404);
+ }
+
+ $owner = $user->walletOwner();
+
+ if (!$user->canDelete($owner)) {
+ return $this->errorResponse(403);
+ }
+
+ $config = $owner->getConfig();
+ $policy_config = [];
+
+ // Get the password policies
+ $policy = new Password($owner);
+ $password_policy = $policy->rules(true);
+ $policy_config['max_password_age'] = $config['max_password_age'];
+
+ // Get the mail delivery policies
+ $mail_delivery_policy = [];
+ if (config('app.with_mailfilter')) {
+ foreach (['itip_policy', 'externalsender_policy'] as $name) {
+ $mail_delivery_policy[] = $name;
+ $policy_config[$name] = $config[$name] ?? null;
+ }
+ }
+
+ return response()->json([
+ 'password' => array_values($password_policy),
+ 'mailDelivery' => $mail_delivery_policy,
+ 'config' => $policy_config,
+ ]);
+ }
+
/**
* SMTP Content Filter
*
diff --git a/src/app/Policy/Mailfilter.php b/src/app/Policy/Mailfilter.php
--- a/src/app/Policy/Mailfilter.php
+++ b/src/app/Policy/Mailfilter.php
@@ -5,12 +5,19 @@
use App\Policy\Mailfilter\MailParser;
use App\Policy\Mailfilter\Modules;
use App\Policy\Mailfilter\Result;
+use App\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Mailfilter
{
+ public const CODE_ACCEPT = 200;
+ public const CODE_ACCEPT_EMPTY = 204;
+ public const CODE_DISCARD = 461;
+ public const CODE_REJECT = 460;
+ public const CODE_ERROR = 500;
+
/**
* SMTP Content Filter
*
@@ -40,11 +47,28 @@
// then we'd send body in another request, but only if needed. For example, a text/plain
// message from same domain sender does not include an iTip, nor needs a footer injection.
+ // Find the recipient user
+ $user = User::where('email', $request->recipient)->first();
+
+ if (empty($user)) {
+ // FIXME: Better code? Should we use custom header instead?
+ return response('', self::CODE_REJECT);
+ }
+
+ // Get list of enabled modules for the recipient user
+ $modules = self::getModulesConfig($user);
+
+ if (empty($modules)) {
+ return response('', self::CODE_ACCEPT_EMPTY);
+ }
+
+ // Handle the mail content from the input
$files = $request->allFiles();
+
if (count($files) == 1) {
$file = $files[array_key_first($files)];
if (!$file->isValid()) {
- return response('Invalid file upload', 500);
+ return response('Invalid file upload', self::CODE_ERROR);
}
$stream = fopen($file->path(), 'r');
@@ -52,35 +76,28 @@
$stream = $request->getContent(true);
}
+ // Initialize mail parser
$parser = new MailParser($stream);
-
- if ($recipient = $request->recipient) {
- $parser->setRecipient($recipient);
- }
+ $parser->setRecipient($user);
if ($sender = $request->sender) {
$parser->setSender($sender);
}
- // TODO: The list of modules and their config will come from somewhere
- $modules = [
- 'itip' => Modules\ItipModule::class,
- 'external-sender' => Modules\ExternalSenderModule::class,
- ];
-
- foreach ($modules as $module) {
- $engine = new $module();
+ // Execute modules
+ foreach ($modules as $module => $config) {
+ $engine = new $module($config);
$result = $engine->handle($parser);
if ($result) {
if ($result->getStatus() == Result::STATUS_REJECT) {
// FIXME: Better code? Should we use custom header instead?
- return response('', 460);
+ return response('', self::CODE_REJECT);
}
if ($result->getStatus() == Result::STATUS_DISCARD) {
// FIXME: Better code? Should we use custom header instead?
- return response('', 461);
+ return response('', self::CODE_DISCARD);
}
}
}
@@ -104,6 +121,33 @@
return $response;
}
- return response('', 204);
+ return response('', self::CODE_ACCEPT_EMPTY);
+ }
+
+ /**
+ * Get list of enabled mail filter modules with their configuration
+ */
+ protected static function getModulesConfig(User $user): array
+ {
+ $modules = [
+ Modules\ItipModule::class => [],
+ Modules\ExternalSenderModule::class => [],
+ ];
+
+ // TODO: Check user configuration before defaulting to account policy
+
+ // Check account policy
+ $config = $user->walletOwner()->getConfig();
+
+ foreach ($modules as $class => $module_config) {
+ $module = strtolower(str_replace('Module', '', class_basename($class)));
+ if (empty($config["{$module}_policy"])) {
+ unset($modules[$class]);
+ }
+
+ // TODO: Collect module configuration
+ }
+
+ return $modules;
}
}
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
@@ -332,10 +332,17 @@
/**
* Set email address of the recipient
+ *
+ * @param string|User $recipient Recipient email address or User object
*/
- public function setRecipient(string $recipient): void
+ public function setRecipient($recipient): void
{
- $this->recipient = $recipient;
+ if ($recipient instanceof User) {
+ $this->user = $recipient;
+ $this->recipient = $recipient->email;
+ } else {
+ $this->recipient = $recipient;
+ }
}
/**
diff --git a/src/app/Policy/Mailfilter/Module.php b/src/app/Policy/Mailfilter/Module.php
new file mode 100644
--- /dev/null
+++ b/src/app/Policy/Mailfilter/Module.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Policy\Mailfilter;
+
+abstract class Module
+{
+ /** @var array Module configuration */
+ protected array $config = [];
+
+ /**
+ * Module constructor
+ */
+ public function __construct(array $config = [])
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * Handle the email message
+ */
+ abstract public function handle(MailParser $parser): ?Result;
+}
diff --git a/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php b/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php
--- a/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php
+++ b/src/app/Policy/Mailfilter/Modules/ExternalSenderModule.php
@@ -3,9 +3,10 @@
namespace App\Policy\Mailfilter\Modules;
use App\Policy\Mailfilter\MailParser;
+use App\Policy\Mailfilter\Module;
use App\Policy\Mailfilter\Result;
-class ExternalSenderModule
+class ExternalSenderModule extends Module
{
/**
* Handle the email message
diff --git a/src/app/Policy/Mailfilter/Modules/ItipModule.php b/src/app/Policy/Mailfilter/Modules/ItipModule.php
--- a/src/app/Policy/Mailfilter/Modules/ItipModule.php
+++ b/src/app/Policy/Mailfilter/Modules/ItipModule.php
@@ -5,6 +5,7 @@
use App\Auth\Utils;
use App\Backends\DAV;
use App\Policy\Mailfilter\MailParser;
+use App\Policy\Mailfilter\Module;
use App\Policy\Mailfilter\Result;
use App\Support\Facades\DAV as DAVFacade;
use App\User;
@@ -12,7 +13,7 @@
use Sabre\VObject\Document;
use Sabre\VObject\Reader;
-class ItipModule
+class ItipModule extends Module
{
protected $davClient;
protected $davFolder;
diff --git a/src/app/Traits/UserConfigTrait.php b/src/app/Traits/UserConfigTrait.php
--- a/src/app/Traits/UserConfigTrait.php
+++ b/src/app/Traits/UserConfigTrait.php
@@ -13,16 +13,20 @@
public function getConfig(): array
{
$settings = $this->getSettings([
+ 'externalsender_policy',
'greylist_enabled',
'guam_enabled',
- 'password_policy',
- 'max_password_age',
+ 'itip_policy',
'limit_geo',
+ 'max_password_age',
+ 'password_policy',
]);
$config = [
+ 'externalsender_policy' => $settings['externalsender_policy'] === 'true',
'greylist_enabled' => $settings['greylist_enabled'] !== 'false',
'guam_enabled' => $settings['guam_enabled'] === 'true',
+ 'itip_policy' => $settings['itip_policy'] === 'true',
'limit_geo' => $settings['limit_geo'] ? json_decode($settings['limit_geo'], true) : [],
'max_password_age' => $settings['max_password_age'],
'password_policy' => $settings['password_policy'],
@@ -43,7 +47,7 @@
$errors = [];
foreach ($config as $key => $value) {
- if ($key == 'greylist_enabled') {
+ if (in_array($key, ['greylist_enabled', 'itip_policy', 'externalsender_policy'])) {
$this->setSetting($key, $value ? 'true' : 'false');
} elseif ($key == 'guam_enabled') {
$this->setSetting($key, $value ? 'true' : null);
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -262,6 +262,7 @@
'with_meet' => (bool) env('APP_WITH_MEET', true),
'with_companion_app' => (bool) env('APP_WITH_COMPANION_APP', true),
'with_user_search' => (bool) env('APP_WITH_USER_SEARCH', false),
+ 'with_mailfilter' => (bool) env('APP_WITH_MAILFILTER', true),
'signup' => [
'email_limit' => (int) env('SIGNUP_LIMIT_EMAIL', 0),
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -375,6 +375,12 @@
],
'policies' => [
+ 'calinvitations' => "Calendar invitations",
+ 'calinvitations-text' => "Enables automated handling of calendar invitations in incoming email.",
+ 'extsender' => "External sender warning",
+ 'extsender-text' => "Adds a warning to every delivered message sent by an external sender.",
+ 'mailDelivery' => "Mail delivery",
+ 'password' => "Password",
'password-policy' => "Password Policy",
'password-retention' => "Password Retention",
'password-max-age' => "Require a password change every",
diff --git a/src/resources/vue/Policies.vue b/src/resources/vue/Policies.vue
--- a/src/resources/vue/Policies.vue
+++ b/src/resources/vue/Policies.vue
@@ -6,43 +6,71 @@
{{ $t('dashboard.policies') }}
</div>
<div class="card-text">
- <form @submit.prevent="submit">
- <div class="row mb-3">
- <label class="col-sm-4 col-form-label">{{ $t('policies.password-policy') }}</label>
- <div class="col-sm-8">
- <ul id="password_policy" class="list-group ms-1 mt-1">
- <li v-for="rule in passwordPolicy" :key="rule.label" class="list-group-item border-0 form-check pt-1 pb-1">
- <input type="checkbox" class="form-check-input"
- :id="'policy-' + rule.label"
- :name="rule.label"
- :checked="rule.enabled || isRequired(rule)"
- :disabled="isRequired(rule)"
- >
- <span v-if="rule.label == 'last'" v-html="ruleLastHTML(rule)"></span>
- <label v-else :for="'policy-' + rule.label" class="form-check-label pe-2" style="opacity: 1;">{{ rule.name.split(':')[0] }}</label>
- <input type="text" class="form-control form-control-sm w-auto d-inline" v-if="['min', 'max'].includes(rule.label)" :value="rule.param" size="3">
- </li>
- </ul>
- </div>
+ <tabs class="mt-3" :tabs="tabs" ref="tabs"></tabs>
+ <div class="tab-content">
+ <div class="tab-pane active" id="password" role="tabpanel" aria-labelledby="tab-password">
+ <form class="card-body" @submit.prevent="submitPassword">
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">{{ $t('policies.password-policy') }}</label>
+ <div class="col-sm-8">
+ <ul id="password_policy" class="list-group ms-1 mt-1">
+ <li v-for="rule in passwordPolicy" :key="rule.label" class="list-group-item border-0 form-check pt-1 pb-1">
+ <input type="checkbox" class="form-check-input"
+ :id="'policy-' + rule.label"
+ :name="rule.label"
+ :checked="rule.enabled || isRequired(rule)"
+ :disabled="isRequired(rule)"
+ >
+ <span v-if="rule.label == 'last'" v-html="ruleLastHTML(rule)"></span>
+ <label v-else :for="'policy-' + rule.label" class="form-check-label pe-2" style="opacity: 1;">{{ rule.name.split(':')[0] }}</label>
+ <input type="text" class="form-control form-control-sm w-auto d-inline" v-if="['min', 'max'].includes(rule.label)" :value="rule.param" size="3">
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">{{ $t('policies.password-retention') }}</label>
+ <div class="col-sm-8">
+ <ul id="password_retention" class="list-group ms-1 mt-1">
+ <li class="list-group-item border-0 form-check pt-1 pb-1">
+ <input type="checkbox" class="form-check-input" id="max_password_age" :checked="config.max_password_age">
+ <label for="max_password_age" class="form-check-label pe-2">{{ $t('policies.password-max-age') }}</label>
+ <select class="form-select form-select-sm d-inline w-auto" id="max_password_age_value">
+ <option v-for="num in [3, 6, 9, 12]" :key="num" :value="num" :selected="num == config.max_password_age">
+ {{ num }} {{ $t('form.months') }}
+ </option>
+ </select>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
</div>
- <div class="row mb-3">
- <label class="col-sm-4 col-form-label">{{ $t('policies.password-retention') }}</label>
- <div class="col-sm-8">
- <ul id="password_retention" class="list-group ms-1 mt-1">
- <li class="list-group-item border-0 form-check pt-1 pb-1">
- <input type="checkbox" class="form-check-input" id="max_password_age" :checked="config.max_password_age">
- <label for="max_password_age" class="form-check-label pe-2">{{ $t('policies.password-max-age') }}</label>
- <select class="form-select form-select-sm d-inline w-auto" id="max_password_age_value">
- <option v-for="num in [3, 6, 9, 12]" :key="num" :value="num" :selected="num == config.max_password_age">
- {{ num }} {{ $t('form.months') }}
- </option>
- </select>
- </li>
- </ul>
- </div>
+ <div class="tab-pane" id="mailDelivery" role="tabpanel" aria-labelledby="tab-mailDelivery">
+ <form class="card-body" @submit.prevent="submitMailDelivery">
+ <div class="row checkbox mb-3">
+ <label for="itip_policy" class="col-sm-4 col-form-label">{{ $t('policies.calinvitations') }}</label>
+ <div class="col-sm-8 pt-2">
+ <input type="checkbox" id="itip_policy" name="itip" value="1" class="form-check-input d-block mb-2" :checked="config.itip_policy">
+ <small id="itip-hint" class="text-muted">
+ {{ $t('policies.calinvitations-text') }}
+ </small>
+ </div>
+ </div>
+ <div class="row checkbox mb-3">
+ <label for="externalsender_policy" class="col-sm-4 col-form-label">{{ $t('policies.extsender') }}</label>
+ <div class="col-sm-8 pt-2">
+ <input type="checkbox" id="externalsender_policy" name="externalsender" value="1" class="form-check-input d-block mb-2" :checked="config.externalsender_policy">
+ <small id="externalsender-hint" class="text-muted">
+ {{ $t('policies.extsender-text') }}
+ </small>
+ </div>
+ </div>
+ <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
</div>
- <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
- </form>
+ </div>
</div>
</div>
</div>
@@ -50,22 +78,31 @@
</template>
<script>
+ const POLICY_TYPES = ['password', 'mailDelivery']
+
export default {
data() {
return {
config: [],
+ mailDeliveryPolicy: [],
passwordPolicy: []
}
},
+ computed: {
+ tabs: function () {
+ return POLICY_TYPES.filter(v => this[v + 'Policy'].length > 0)
+ .map(v => 'policies.' + v);
+ }
+ },
created() {
this.wallet = this.$root.authInfo.wallet
},
mounted() {
- axios.get('/api/v4/password-policy', { loader: true })
+ axios.get('/api/v4/policies', { loader: true })
.then(response => {
- if (response.data.list) {
- this.passwordPolicy = response.data.list
+ if (response.data.config) {
this.config = response.data.config
+ POLICY_TYPES.forEach(element => this[element + 'Policy'] = response.data[element])
}
})
.catch(this.$root.errorHandler)
@@ -87,8 +124,19 @@
${parts[0]} <select class="form-select form-select-sm d-inline w-auto">${options.join('')}</select> ${parts[1]}
</label>`
},
- submit() {
- this.$root.clearFormValidation($('#policies form'))
+ submitMailDelivery() {
+ this.$root.clearFormValidation($('#maildelivery form'))
+
+ let post = {}
+ this.mailDeliveryPolicy.forEach(element => post[element] = $('#' + element)[0].checked)
+
+ axios.post('/api/v4/users/' + this.wallet.user_id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
+ },
+ submitPassword() {
+ this.$root.clearFormValidation($('#password form'))
let max_password_age = $('#max_password_age:checked').length ? $('#max_password_age_value').val() : 0
let password_policy = [];
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -168,7 +168,7 @@
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\WalletsController::class, 'receiptDownload']);
Route::get('wallets/{id}/referral-programs', [API\V4\WalletsController::class, 'referralPrograms']);
- Route::get('password-policy', [API\PasswordPolicyController::class, 'index']);
+ Route::get('policies', [API\V4\PolicyController::class, 'index']);
Route::post('password-reset/code', [API\PasswordResetController::class, 'codeCreate']);
Route::delete('password-reset/code/{id}', [API\PasswordResetController::class, 'codeDelete']);
diff --git a/src/tests/Browser/Pages/Policies.php b/src/tests/Browser/Pages/Policies.php
--- a/src/tests/Browser/Pages/Policies.php
+++ b/src/tests/Browser/Pages/Policies.php
@@ -22,7 +22,7 @@
*/
public function assert($browser)
{
- $browser->waitFor('@form')
+ $browser->waitFor('@password-form')
->waitUntilMissing('.app-loader');
}
@@ -33,7 +33,8 @@
{
return [
'@app' => '#app',
- '@form' => '#policies form',
+ '@password-form' => '#password form',
+ '@maildelivery-form' => '#mailDelivery form',
];
}
}
diff --git a/src/tests/Browser/PoliciesTest.php b/src/tests/Browser/PoliciesTest.php
--- a/src/tests/Browser/PoliciesTest.php
+++ b/src/tests/Browser/PoliciesTest.php
@@ -64,9 +64,10 @@
$browser->click('@links .link-policies')
->on(new Policies())
->assertSeeIn('#policies .card-title', 'Policies')
+ ->assertSeeIn('#policies .nav-item:nth-child(1)', 'Password')
// Password policy
- ->assertSeeIn('@form .row:nth-child(1) > label', 'Password Policy')
- ->with('@form #password_policy', static function (Browser $browser) {
+ ->assertSeeIn('@password-form .row:nth-child(1) > label', 'Password Policy')
+ ->with('@password-form #password_policy', static function (Browser $browser) {
$browser->assertElementsCount('li', 7)
->assertSeeIn('li:nth-child(1) label', 'Minimum password length')
->assertChecked('li:nth-child(1) input[type=checkbox]')
@@ -99,8 +100,8 @@
->click('li:nth-child(3) input[type=checkbox]')
->click('li:nth-child(4) input[type=checkbox]');
})
- ->assertSeeIn('@form .row:nth-child(2) > label', 'Password Retention')
- ->with('@form #password_retention', static function (Browser $browser) {
+ ->assertSeeIn('@password-form .row:nth-child(2) > label', 'Password Retention')
+ ->with('@password-form #password_retention', static function (Browser $browser) {
$browser->assertElementsCount('li', 1)
->assertSeeIn('li:nth-child(1) label', 'Require a password change every')
->assertNotChecked('li:nth-child(1) input[type=checkbox]')
@@ -110,7 +111,7 @@
->check('li:nth-child(1) input[type=checkbox]')
->select('li:nth-child(1) select', 6);
})
- ->click('button[type=submit]')
+ ->click('@password-form button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.');
});
diff --git a/src/tests/Feature/Controller/PasswordPolicyTest.php b/src/tests/Feature/Controller/PasswordPolicyTest.php
--- a/src/tests/Feature/Controller/PasswordPolicyTest.php
+++ b/src/tests/Feature/Controller/PasswordPolicyTest.php
@@ -64,77 +64,4 @@
$this->assertTrue($json['list'][1]['status']);
$this->assertSame('max', $json['list'][1]['label']);
}
-
- /**
- * Test password-policy listing
- */
- public function testIndex(): void
- {
- // Unauth access not allowed
- $response = $this->get('/api/v4/password-policy');
- $response->assertStatus(401);
-
- $jack = $this->getTestUser('jack@kolab.org');
- $john = $this->getTestUser('john@kolab.org');
- $john->setSetting('password_policy', 'min:8,max:255,special');
- $john->setSetting('max_password_age', 6);
-
- // Get available policy rules
- $response = $this->actingAs($john)->get('/api/v4/password-policy');
- $json = $response->json();
-
- $response->assertStatus(200);
-
- $this->assertCount(3, $json);
- $this->assertSame(7, $json['count']);
- $this->assertCount(7, $json['list']);
- $this->assertSame(['max_password_age' => '6'], $json['config']);
- $this->assertSame('Minimum password length: 8 characters', $json['list'][0]['name']);
- $this->assertSame('min', $json['list'][0]['label']);
- $this->assertSame('8', $json['list'][0]['param']);
- $this->assertTrue($json['list'][0]['enabled']);
- $this->assertSame('Maximum password length: 255 characters', $json['list'][1]['name']);
- $this->assertSame('max', $json['list'][1]['label']);
- $this->assertSame('255', $json['list'][1]['param']);
- $this->assertTrue($json['list'][1]['enabled']);
- $this->assertSame('lower', $json['list'][2]['label']);
- $this->assertFalse($json['list'][2]['enabled']);
- $this->assertSame('upper', $json['list'][3]['label']);
- $this->assertFalse($json['list'][3]['enabled']);
- $this->assertSame('digit', $json['list'][4]['label']);
- $this->assertFalse($json['list'][4]['enabled']);
- $this->assertSame('special', $json['list'][5]['label']);
- $this->assertTrue($json['list'][5]['enabled']);
- $this->assertSame('last', $json['list'][6]['label']);
- $this->assertFalse($json['list'][6]['enabled']);
-
- // Test acting as Jack
- $response = $this->actingAs($jack)->get('/api/v4/password-policy');
- $json = $response->json();
-
- $response->assertStatus(200);
-
- $this->assertCount(3, $json);
- $this->assertSame(7, $json['count']);
- $this->assertCount(7, $json['list']);
- $this->assertSame(['max_password_age' => '6'], $json['config']);
- $this->assertSame('Minimum password length: 8 characters', $json['list'][0]['name']);
- $this->assertSame('min', $json['list'][0]['label']);
- $this->assertSame('8', $json['list'][0]['param']);
- $this->assertTrue($json['list'][0]['enabled']);
- $this->assertSame('Maximum password length: 255 characters', $json['list'][1]['name']);
- $this->assertSame('max', $json['list'][1]['label']);
- $this->assertSame('255', $json['list'][1]['param']);
- $this->assertTrue($json['list'][1]['enabled']);
- $this->assertSame('lower', $json['list'][2]['label']);
- $this->assertFalse($json['list'][2]['enabled']);
- $this->assertSame('upper', $json['list'][3]['label']);
- $this->assertFalse($json['list'][3]['enabled']);
- $this->assertSame('digit', $json['list'][4]['label']);
- $this->assertFalse($json['list'][4]['enabled']);
- $this->assertSame('special', $json['list'][5]['label']);
- $this->assertTrue($json['list'][5]['enabled']);
- $this->assertSame('last', $json['list'][6]['label']);
- $this->assertFalse($json['list'][6]['enabled']);
- }
}
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
@@ -52,6 +52,10 @@
Greylist\Connect::where('sender_domain', 'sender.domain')->delete();
Greylist\Whitelist::where('sender_domain', 'sender.domain')->delete();
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()
+ ->whereIn('key', ['password_policy', 'max_password_age', 'itip_policy', 'externalsender_policy'])->delete();
+
parent::tearDown();
}
@@ -94,6 +98,95 @@
$this->assertMatchesRegularExpression('/^Received-Greylist: greylisted from/', $json['prepend'][0]);
}
+ /**
+ * Test fetching account 'password' policies
+ */
+ public function testIndexPassword(): void
+ {
+ $this->useRegularUrl();
+
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSetting('password_policy', 'min:8,max:255,special');
+ $john->setSetting('max_password_age', 6);
+
+ // Unauth access not allowed
+ $response = $this->get('/api/v4/policies');
+ $response->assertStatus(401);
+
+ // Test acting as non-controller
+ $response = $this->actingAs($jack)->get('/api/v4/policies');
+ $response->assertStatus(403);
+
+ // Get available policy rules
+ $response = $this->actingAs($john)->get('/api/v4/policies');
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertCount(7, $json['password']);
+ $this->assertSame('6', $json['config']['max_password_age']);
+ $this->assertSame('Minimum password length: 8 characters', $json['password'][0]['name']);
+ $this->assertSame('min', $json['password'][0]['label']);
+ $this->assertSame('8', $json['password'][0]['param']);
+ $this->assertTrue($json['password'][0]['enabled']);
+ $this->assertSame('Maximum password length: 255 characters', $json['password'][1]['name']);
+ $this->assertSame('max', $json['password'][1]['label']);
+ $this->assertSame('255', $json['password'][1]['param']);
+ $this->assertTrue($json['password'][1]['enabled']);
+ $this->assertSame('lower', $json['password'][2]['label']);
+ $this->assertFalse($json['password'][2]['enabled']);
+ $this->assertSame('upper', $json['password'][3]['label']);
+ $this->assertFalse($json['password'][3]['enabled']);
+ $this->assertSame('digit', $json['password'][4]['label']);
+ $this->assertFalse($json['password'][4]['enabled']);
+ $this->assertSame('special', $json['password'][5]['label']);
+ $this->assertTrue($json['password'][5]['enabled']);
+ $this->assertSame('last', $json['password'][6]['label']);
+ $this->assertFalse($json['password'][6]['enabled']);
+ }
+
+ /**
+ * Test fetching account 'mailDelivery' policies
+ */
+ public function testIndexMailDelivery(): void
+ {
+ $this->useRegularUrl();
+
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->whereIn('key', ['itip_policy', 'externalsender_policy'])->delete();
+
+ // Unauth access not allowed
+ $response = $this->get('/api/v4/policies');
+ $response->assertStatus(401);
+
+ // Test acting as non-controller
+ $response = $this->actingAs($jack)->get('/api/v4/policies');
+ $response->assertStatus(403);
+
+ // Get polcies when mailfilter is disabled
+ \config(['app.with_mailfilter' => false]);
+ $response = $this->actingAs($john)->get('/api/v4/policies');
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertCount(0, $json['mailDelivery']);
+
+ // Get polcies when mailfilter is enabled
+ \config(['app.with_mailfilter' => true]);
+ $john->setConfig(['externalsender_policy' => true]);
+ $response = $this->actingAs($john)->get('/api/v4/policies');
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertSame(['itip_policy', 'externalsender_policy'], $json['mailDelivery']);
+ $this->assertFalse($json['config']['itip_policy']);
+ $this->assertTrue($json['config']['externalsender_policy']);
+ }
+
/**
* Test mail filter (POST /api/webhooks/policy/mail/filter)
*/
@@ -102,15 +195,18 @@
// Note: Only basic tests here. More detailed policy handler tests are in another place
$headers = ['CONTENT_TYPE' => 'message/rfc822'];
- $post = file_get_contents(__DIR__ . '/../../data/mail/1.eml');
+ $post = file_get_contents(self::BASE_DIR . '/data/mail/1.eml');
$post = str_replace("\n", "\r\n", $post);
+ $john = $this->getTestUser('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)
->assertNoContent(204);
// Test returning (modified) mail content
+ $john->setConfig(['externalsender_policy' => true]);
$url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org&sender=jack@external.tld';
$content = $this->call('POST', $url, [], [], [], $headers, $post)
->assertStatus(200)
@@ -118,11 +214,6 @@
->streamedContent();
$this->assertStringContainsString('Subject: [EXTERNAL] test sync', $content);
-
- // TODO: Test multipart/form-data request
- // TODO: Test rejecting mail
- // TODO: Test two modules that both modify the mail content
- $this->markTestIncomplete();
}
/**
diff --git a/src/tests/Feature/Policy/MailfilterTest.php b/src/tests/Feature/Policy/MailfilterTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Policy/MailfilterTest.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Tests\Feature\Policy;
+
+use App\Policy\Mailfilter;
+use Illuminate\Http\Request;
+use Illuminate\Http\UploadedFile;
+use Tests\TestCase;
+
+class MailfilterTest extends TestCase
+{
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->whereIn('key', ['itip_policy', 'externalsender_policy'])->delete();
+ }
+
+ protected function tearDown(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->whereIn('key', ['itip_policy', 'externalsender_policy'])->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test mail filter basic functionality
+ */
+ public function testHandle()
+ {
+ $mail = file_get_contents(self::BASE_DIR . '/data/mail/1.eml');
+ $mail = str_replace("\n", "\r\n", $mail);
+
+ // Test unknown recipient
+ $get = ['recipient' => 'unknown@domain.tld', 'sender' => 'jack@kolab.org'];
+ $request = new Request($get, [], [], [], [], [], $mail);
+ $response = Mailfilter::handle($request);
+
+ $this->assertSame(Mailfilter::CODE_REJECT, $response->status());
+ $this->assertSame('', $response->content());
+
+ $john = $this->getTestUser('john@kolab.org');
+
+ // No modules enabled, no changes to the mail content
+ $get = ['recipient' => $john->email, 'sender' => 'jack@kolab.org'];
+ $request = new Request($get, [], [], [], [], [], $mail);
+ $response = Mailfilter::handle($request);
+
+ $this->assertSame(Mailfilter::CODE_ACCEPT_EMPTY, $response->status());
+ $this->assertSame('', $response->content());
+
+ // Note: We using HTTP controller here for easier use of Laravel request/response
+ $this->useServicesUrl();
+
+ // Test returning (modified) mail content
+ $john->setConfig(['externalsender_policy' => true]);
+ $url = '/api/webhooks/policy/mail/filter?recipient=john@kolab.org&sender=jack@external.tld';
+ $content = $this->call('POST', $url, [], [], [], [], $mail)
+ ->assertStatus(200)
+ ->assertHeader('Content-Type', 'message/rfc822')
+ ->streamedContent();
+
+ $this->assertStringContainsString('Subject: [EXTERNAL] test sync', $content);
+ $this->assertStringContainsString('ZWVlYQ==', $content);
+
+ // Test multipart/form-data request
+ $file = UploadedFile::fake()->createWithContent('mail.eml', $mail);
+ $content = $this->call('POST', $url, ['file' => $file], [], [], [])
+ ->assertStatus(200)
+ ->assertHeader('Content-Type', 'message/rfc822')
+ ->streamedContent();
+
+ $this->assertStringContainsString('Subject: [EXTERNAL] test sync', $content);
+ $this->assertStringContainsString('ZWVlYQ==', $content);
+
+ // TODO: Test rejecting mail
+ // TODO: Test two modules that both modify the mail content
+ $this->markTestIncomplete();
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 5:25 PM (13 h, 30 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830551
Default Alt Text
D5262.1775323559.diff (39 KB)

Event Timeline