Page MenuHomePhorge

D5262.1775252903.diff
No OneTemporary

Authored By
Unknown
Size
61 KB
Referenced Files
None
Subscribers
None

D5262.1775252903.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/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

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)

Event Timeline