Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117867846
D5262.1775323559.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
39 KB
Referenced Files
None
Subscribers
None
D5262.1775323559.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D5262: WIP: Mail delivery policies UI
Attached
Detach File
Event Timeline