Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117786519
D5262.1775252903.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
61 KB
Referenced Files
None
Subscribers
None
D5262.1775252903.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/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -258,6 +258,7 @@
'enableDistlists' => $isController && $hasCustomDomain && \config('app.with_distlists'),
'enableFiles' => !$isDegraded && $hasBeta && \config('app.with_files'),
'enableFolders' => $isController && $hasCustomDomain && \config('app.with_shared_folders'),
+ 'enableMailfilter' => $isController && config('app.with_mailfilter'),
'enableResources' => $isController && $hasCustomDomain && $hasBeta && \config('app.with_resources'),
'enableRooms' => $hasMeet,
'enableSettings' => $isController,
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,46 @@
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 => [],
+ ];
+
+ // Get user configuration (and policy if it is the account owner)
+ $config = $user->getConfig();
+
+ // Include account policy (if it is not the account owner)
+ $wallet = $user->wallet();
+ if ($wallet->user_id != $user->id) {
+ $policy = array_filter(
+ $wallet->owner->getConfig(),
+ fn ($key) => str_contains($key, 'policy'),
+ \ARRAY_FILTER_USE_KEY
+ );
+
+ $config = array_merge($config, $policy);
+ }
+
+ foreach ($modules as $class => $module_config) {
+ $module = strtolower(str_replace('Module', '', class_basename($class)));
+ if (
+ (isset($config["{$module}_config"]) && $config["{$module}_config"] === false)
+ || (!isset($config["{$module}_config"]) && 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,24 @@
public function getConfig(): array
{
$settings = $this->getSettings([
+ 'externalsender_config',
+ 'externalsender_policy',
'greylist_enabled',
'guam_enabled',
- 'password_policy',
- 'max_password_age',
+ 'itip_config',
+ 'itip_policy',
'limit_geo',
+ 'max_password_age',
+ 'password_policy',
]);
$config = [
+ 'externalsender_config' => self::boolOrNull($settings['externalsender_config']),
+ 'externalsender_policy' => $settings['externalsender_policy'] === 'true',
'greylist_enabled' => $settings['greylist_enabled'] !== 'false',
'guam_enabled' => $settings['guam_enabled'] === 'true',
+ 'itip_config' => self::boolOrNull($settings['itip_config']),
+ '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,8 +51,10 @@
$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 (in_array($key, ['itip_config', 'externalsender_config'])) {
+ $this->setSetting($key, $value === null ? null : ($value ? 'true' : 'false'));
} elseif ($key == 'guam_enabled') {
$this->setSetting($key, $value ? 'true' : null);
} elseif ($key == 'limit_geo') {
@@ -84,6 +94,14 @@
return $errors;
}
+ /**
+ * Convert string value into real value (bool or null)
+ */
+ private static function boolOrNull($value): ?bool
+ {
+ return $value === 'true' ? true : ($value === 'false' ? false : null);
+ }
+
/**
* Validates password policy rule.
*
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
@@ -195,6 +195,7 @@
'comment' => "Comment",
'companion' => "Companion App",
'date' => "Date",
+ 'default' => "default",
'description' => "Description",
'details' => "Details",
'disabled' => "disabled",
@@ -208,7 +209,7 @@
'lastname' => "Last Name",
'less' => "Less",
'name' => "Name",
- 'mainopts' => "Main Options",
+ 'mainopts' => "Main options",
'months' => "months",
'more' => "More",
'none' => "none",
@@ -375,6 +376,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/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -92,15 +92,6 @@
<accordion class="mt-3" id="settings-all" :names="settingsSections" :buttons="settingsButtons">
<template #options v-if="settingsSections.options">
<form @submit.prevent="submitSettings">
- <div class="row checkbox mb-3">
- <label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
- <div class="col-sm-8 pt-2">
- <input type="checkbox" id="greylist_enabled" name="greylist_enabled" value="1" class="form-check-input d-block mb-2" :checked="user.config.greylist_enabled">
- <small id="greylisting-hint" class="text-muted">
- {{ $t('user.greylisting-text') }}
- </small>
- </div>
- </div>
<div v-if="$root.hasPermission('beta')" class="row checkbox mb-3">
<label for="guam_enabled" class="col-sm-4 col-form-label">
{{ $t('user.imapproxy') }}
@@ -128,6 +119,46 @@
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
</template>
+ <template #maildelivery v-if="settingsSections.maildelivery">
+ <form @submit.prevent="submitMailDelivery">
+ <div class="row checkbox mb-3">
+ <label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
+ <div class="col-sm-8 pt-2">
+ <input type="checkbox" id="greylist_enabled" name="greylist_enabled" value="1" class="form-check-input d-block mb-2" :checked="user.config.greylist_enabled">
+ <small id="greylisting-hint" class="text-muted">
+ {{ $t('user.greylisting-text') }}
+ </small>
+ </div>
+ </div>
+ <div class="row mb-3" v-if="$root.authInfo.statusInfo.enableMailfilter">
+ <label for="itip_config" class="col-sm-4 col-form-label">{{ $t('policies.calinvitations') }}</label>
+ <div class="col-sm-8">
+ <select id="itip_config" name="itip" class="form-select">
+ <option value="" :selected="user.config.itip_config == null">{{ $t('form.default') }}</option>
+ <option value="true" :selected="user.config.itip_config === true">{{ $t('form.enabled') }}</option>
+ <option value="false" :selected="user.config.itip_config === false">{{ $t('form.disabled') }}</option>
+ </select>
+ <small id="itip-hint" class="text-muted">
+ {{ $t('policies.calinvitations-text') }}
+ </small>
+ </div>
+ </div>
+ <div class="row mb-3" v-if="$root.authInfo.statusInfo.enableMailfilter">
+ <label for="externalsender_config" class="col-sm-4 col-form-label">{{ $t('policies.extsender') }}</label>
+ <div class="col-sm-8">
+ <select id="externalsender_config" name="extsender" class="form-select">
+ <option value="" :selected="user.config.externalsender_config == null">{{ $t('form.default') }}</option>
+ <option value="true" :selected="user.config.externalsender_config === true">{{ $t('form.enabled') }}</option>
+ <option value="false" :selected="user.config.externalsender_config === false">{{ $t('form.disabled') }}</option>
+ </select>
+ <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>
+ </template>
<template #delegation v-if="settingsSections.delegation">
<list-table :list="delegations" :setup="delegationListSetup" class="mb-0">
<template #email="{ item }">
@@ -321,7 +352,10 @@
settingsSections: function () {
let opts = {}
if (this.isController) {
- opts.options = this.$t('form.mainopts')
+ if (this.$root.hasPermission('beta')) {
+ opts.options = this.$t('form.mainopts')
+ }
+ opts.maildelivery = this.$t('policies.mailDelivery')
}
if ((this.isController || this.isSelf) && this.$root.authInfo.statusInfo.enableDelegation) {
opts.delegation = this.$t('user.delegation')
@@ -481,6 +515,25 @@
}
})
},
+ submitMailDelivery() {
+ this.$root.clearFormValidation('#maildelivery form')
+
+ const typeMap = { 'true': true, 'false': false }
+ let post = {}
+
+ $('#maildelivery form').find('select,input[type=checkbox]').each(function() {
+ if (this.nodeName == 'INPUT') {
+ post[this.id] = this.checked ? 1 : 0
+ } else {
+ post[this.id] = typeMap[this.value] || null
+ }
+ })
+
+ axios.post('/api/v4/users/' + this.user_id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
+ },
submitPersonalSettings() {
this.$root.clearFormValidation($('#personal form'))
@@ -504,8 +557,7 @@
let post = this.$root.pick(this.user.config, ['limit_geo'])
- const checklist = ['greylist_enabled', 'guam_enabled']
- checklist.forEach(name => {
+ ['guam_enabled'].forEach(name => {
if ($('#' + name).length) {
post[name] = $('#' + name).prop('checked') ? 1 : 0
}
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/Pages/UserInfo.php b/src/tests/Browser/Pages/UserInfo.php
--- a/src/tests/Browser/Pages/UserInfo.php
+++ b/src/tests/Browser/Pages/UserInfo.php
@@ -36,6 +36,8 @@
'@nav' => 'ul.nav-tabs',
'@packages' => '#user-packages',
'@settings' => '#settings',
+ '@setting-maildelivery' => '#maildelivery .accordion-body',
+ '@setting-maildelivery-head' => '#maildelivery-header',
'@setting-options' => '#options .accordion-body',
'@setting-options-head' => '#options-header',
'@setting-delegation' => '#delegation .accordion-body',
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
@@ -12,6 +12,17 @@
class PoliciesTest extends TestCaseDusk
{
+ protected function tearDown(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSettings([
+ 'itip_policy' => null,
+ 'externalsender_policy' => null,
+ ]);
+
+ parent::tearDown();
+ }
+
/**
* Test Policies page (unauthenticated)
*/
@@ -50,11 +61,11 @@
}
/**
- * Test Policies page
+ * Test password policies page
*
* @depends testDashboard
*/
- public function testPolicies(): void
+ public function testPasswordPolicies(): void
{
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('password_policy', 'min:5,max:100,lower');
@@ -64,9 +75,9 @@
$browser->click('@links .link-policies')
->on(new Policies())
->assertSeeIn('#policies .card-title', 'Policies')
- // Password policy
- ->assertSeeIn('@form .row:nth-child(1) > label', 'Password Policy')
- ->with('@form #password_policy', static function (Browser $browser) {
+ ->assertSeeIn('#policies .nav-item:nth-child(1)', 'Password')
+ ->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 +110,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,11 +121,48 @@
->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.');
});
$this->assertSame('min:11,max:120,upper', $john->getSetting('password_policy'));
$this->assertSame('6', $john->getSetting('max_password_age'));
}
+
+ /**
+ * Test maildelivery policies page
+ *
+ * @depends testDashboard
+ */
+ public function testMailDeliveryPolicies(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSettings([
+ 'itip_policy' => 'true',
+ 'externalsender_policy' => null,
+ ]);
+
+ $this->browse(static function (Browser $browser) {
+ $browser->visit('/policies')
+ ->on(new Policies())
+ ->assertSeeIn('#policies .card-title', 'Policies')
+ ->assertSeeIn('#policies .nav-item:nth-child(2)', 'Mail delivery')
+ ->click('#policies .nav-item:nth-child(2)')
+ ->with('@maildelivery-form', static function (Browser $browser) {
+ $browser->assertElementsCount('div.row', 2)
+ ->assertSeeIn('div.row:nth-child(1) label', 'Calendar invitations')
+ ->assertChecked('div.row:nth-child(1) input[type=checkbox]')
+ ->assertSeeIn('div.row:nth-child(2) label', 'External sender warning')
+ ->assertNotChecked('div.row:nth-child(2) input[type=checkbox]')
+ // Change the policy
+ ->click('div.row:nth-child(1) input[type=checkbox]')
+ ->click('div.row:nth-child(2) input[type=checkbox]')
+ ->click('button[type=submit]');
+ })
+ ->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.');
+ });
+
+ $this->assertSame('false', $john->getSetting('itip_policy'));
+ $this->assertSame('true', $john->getSetting('externalsender_policy'));
+ }
}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -35,6 +35,8 @@
'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005",
'external_email' => 'john.doe.external@gmail.com',
'phone' => '+1 509-248-1111',
+ 'itip_config' => null,
+ 'externalsender_config' => null,
];
protected function setUp(): void
@@ -414,41 +416,54 @@
$john->setSetting('greylist_enabled', null);
$john->setSetting('guam_enabled', null);
$john->setSetting('limit_geo', null);
+ $john->setSetting('externalsender_config', 'false');
+ // Mail delivery section
$this->browse(static function (Browser $browser) use ($john) {
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->assertSeeIn('@nav #tab-settings', 'Settings')
->click('@nav #tab-settings')
- ->assertSeeIn('@setting-options-head', 'Main Options')
- ->with('@setting-options', static function (Browser $browser) {
+ ->assertMissing('@setting-options-head') // all main options are hidden
+ ->assertMissing('@setting-options')
+ ->assertSeeIn('@setting-maildelivery-head', 'Mail delivery')
+ ->with('@setting-maildelivery', static function (Browser $browser) {
$browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting')
- ->assertMissing('div.row:nth-child(2)') // guam and geo-lockin settings are hidden
->click('div.row:nth-child(1) input[type=checkbox]:checked')
+ ->assertSeeIn('div.row:nth-child(2) label', 'Calendar invitations')
+ ->assertSelectHasOptions('div.row:nth-child(2) select', ['', 'true', 'false'])
+ ->assertSelected('div.row:nth-child(2) select', '')
+ ->select('div.row:nth-child(2) select', 'true')
+ ->assertSeeIn('div.row:nth-child(3) label', 'External sender warning')
+ ->assertSelectHasOptions('div.row:nth-child(3) select', ['', 'true', 'false'])
+ ->assertSelected('div.row:nth-child(3) select', 'false')
+ ->select('div.row:nth-child(3) select', '')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.');
});
});
$this->assertSame('false', $john->getSetting('greylist_enabled'));
+ $this->assertSame('true', $john->getSetting('itip_config'));
+ $this->assertNull($john->getSetting('externalsender_config'));
+ // Main options section
$this->addBetaEntitlement($john);
-
$this->browse(function (Browser $browser) use ($john) {
$browser->refresh()
->on(new UserInfo())
->click('@nav #tab-settings')
+ ->assertSeeIn('@setting-options-head', 'Main options')
->with('@setting-options', function (Browser $browser) use ($john) {
- $browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting')
- ->assertSeeIn('div.row:nth-child(2) label', 'IMAP proxy')
- ->assertNotChecked('div.row:nth-child(2) input')
- ->assertSeeIn('div.row:nth-child(3) label', 'Geo-lockin')
+ $browser->assertSeeIn('div.row:nth-child(1) label', 'IMAP proxy')
+ ->assertNotChecked('div.row:nth-child(1) input')
+ ->assertSeeIn('div.row:nth-child(2) label', 'Geo-lockin')
->with(new CountrySelect('#limit_geo'), static function ($browser) {
$browser->assertCountries([])
->setCountries(['CH', 'PL'])
->assertCountries(['CH', 'PL']);
})
- ->click('div.row:nth-child(2) input')
+ ->click('div.row:nth-child(1) input')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.');
@@ -460,7 +475,7 @@
$browser->setCountries([])
->assertCountries([]);
})
- ->click('div.row:nth-child(2) input')
+ ->click('div.row:nth-child(1) input')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.');
@@ -876,9 +891,11 @@
->click('@links .link-settings')
->on(new UserInfo())
->click('@nav #tab-settings')
- // Note: Jack should not see Main Options
+ // Note: Jack should not see Main options nor Mail delivery
->assertMissing('@setting-options')
->assertMissing('@setting-options-head')
+ ->assertMissing('@setting-maildelivery')
+ ->assertMissing('@setting-maildelivery-head')
->assertSeeIn('@setting-delegation-head', 'Delegation')
// ->click('@settings .accordion-item:nth-child(2) .accordion-button')
->whenAvailable('@setting-delegation', static function (Browser $browser) {
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,142 @@
+<?php
+
+namespace Tests\Feature\Policy;
+
+use App\Policy\Mailfilter;
+use Illuminate\Http\Request;
+use Illuminate\Http\UploadedFile;
+use Tests\TestCase;
+
+class MailfilterTest extends TestCase
+{
+ private $keys = [
+ 'externalsender_config',
+ 'externalsender_policy',
+ 'itip_config',
+ 'itip_policy',
+ ];
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->whereIn('key', $this->keys)->delete();
+ $jack = $this->getTestUser('jack@kolab.org');
+ $jack->settings()->whereIn('key', $this->keys)->delete();
+ }
+
+ protected function tearDown(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->whereIn('key', $this->keys)->delete();
+ $jack = $this->getTestUser('jack@kolab.org');
+ $jack->settings()->whereIn('key', $this->keys)->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();
+ }
+
+ /**
+ * Test reading modules configuration/policy
+ */
+ public function testGetModulesConfig()
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $filter = new Mailfilter();
+
+ // No module configured yet, no policy, no config
+ $this->assertSame([], $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame([], $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+
+ // Enable account policies
+ $john->setConfig(['externalsender_policy' => true, 'itip_policy' => true]);
+ $expected = [
+ Mailfilter\Modules\ItipModule::class => [],
+ Mailfilter\Modules\ExternalSenderModule::class => [],
+ ];
+
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+
+ // Enabled account policies, and enabled per-user config
+ $jack->setConfig(['externalsender_config' => true, 'itip_config' => true]);
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+
+ // Enabled account policies, and disabled per-user config
+ $jack->setConfig(['externalsender_config' => false, 'itip_config' => false]);
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame([], $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+
+ // Disabled account policies, and disabled per-user config
+ $john->setConfig(['externalsender_policy' => false, 'itip_policy' => false]);
+ $this->assertSame([], $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame([], $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+
+ // Disabled account policies, and enabled per-user config
+ $jack->setConfig(['externalsender_config' => true, 'itip_config' => true]);
+ $this->assertSame([], $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+
+ // As the last one, but for account owner
+ $john->setConfig(['externalsender_config' => true, 'itip_config' => true]);
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$john]));
+ $this->assertSame($expected, $this->invokeMethod($filter, 'getModulesConfig', [$jack]));
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 9:48 PM (20 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826797
Default Alt Text
D5262.1775252903.diff (61 KB)
Attached To
Mode
D5262: WIP: Mail delivery policies UI
Attached
Detach File
Event Timeline