implementation start of rspam/antispam feature
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
@@ -1407,7 +1407,7 @@ class Domains extends ApiCommand implements ResourceEntity
|
||||
$zonefile = $result['zonefile'];
|
||||
}
|
||||
|
||||
if (Settings::Get('dkim.use_dkim') != '1') {
|
||||
if (Settings::Get('antispam.activated') != '1') {
|
||||
$dkim = $result['dkim'];
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@ namespace Froxlor\Api\Commands;
|
||||
use Exception;
|
||||
use Froxlor\Api\ApiCommand;
|
||||
use Froxlor\Api\ResourceEntity;
|
||||
use Froxlor\Cron\TaskId;
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\FroxlorLogger;
|
||||
use Froxlor\Idna\IdnaWrapper;
|
||||
use Froxlor\Settings;
|
||||
use Froxlor\System\Cronjob;
|
||||
use Froxlor\UI\Response;
|
||||
use Froxlor\Validate\Validate;
|
||||
use PDO;
|
||||
@@ -49,6 +51,14 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
* name of the address before @
|
||||
* @param string $domain
|
||||
* domain-name for the email-address
|
||||
* @param float $spam_tag_level
|
||||
* optional, score which is required to tag emails as spam, default: 7.0
|
||||
* @param float $spam_kill_level
|
||||
* optional, score which is required to discard emails, default: 14.0
|
||||
* @param boolean $bypass_spam
|
||||
* optional, disable spam-filter entirely, default: no
|
||||
* @param boolean $policy_greylist
|
||||
* optional, enable grey-listing, default: yes
|
||||
* @param boolean $iscatchall
|
||||
* optional, make this address a catchall address, default: no
|
||||
* @param int $customerid
|
||||
@@ -74,6 +84,10 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
$domain = $this->getParam('domain');
|
||||
|
||||
// parameters
|
||||
$spam_tag_level = $this->getParam('spam_tag_level', true, '7.0');
|
||||
$spam_kill_level = $this->getParam('spam_kill_level', true, '14.0');
|
||||
$bypass_spam = $this->getBoolParam('bypass_spam', true, 0);
|
||||
$policy_greylist = $this->getBoolParam('policy_greylist', true, 1);
|
||||
$iscatchall = $this->getBoolParam('iscatchall', true, 0);
|
||||
$description = $this->getParam('description', true, '');
|
||||
|
||||
@@ -140,11 +154,19 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
}
|
||||
}
|
||||
|
||||
$spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1,2})?$/', '', [7.0], true);
|
||||
$spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1,2})?$/', '', [14.0], true);
|
||||
$description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true);
|
||||
|
||||
$stmt = Database::prepare("
|
||||
INSERT INTO `" . TABLE_MAIL_VIRTUAL . "` SET
|
||||
`customerid` = :cid,
|
||||
`email` = :email,
|
||||
`email_full` = :email_full,
|
||||
`spam_tag_level` = :spam_tag_level,
|
||||
`spam_kill_level` = :spam_kill_level,
|
||||
`bypass_spam` = :bypass_spam,
|
||||
`policy_greylist` = :policy_greylist,
|
||||
`iscatchall` = :iscatchall,
|
||||
`domainid` = :domainid,
|
||||
`description` = :description
|
||||
@@ -153,6 +175,10 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
"cid" => $customer['customerid'],
|
||||
"email" => $email,
|
||||
"email_full" => $email_full,
|
||||
"spam_tag_level" => $spam_tag_level,
|
||||
"spam_kill_level" => $spam_kill_level,
|
||||
"bypass_spam" => $bypass_spam,
|
||||
"policy_greylist" => $policy_greylist,
|
||||
"iscatchall" => $iscatchall,
|
||||
"domainid" => $domain_check['id'],
|
||||
"description" => $description
|
||||
@@ -162,6 +188,7 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
// update customer usage
|
||||
Customers::increaseUsage($customer['customerid'], 'emails_used');
|
||||
|
||||
Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
|
||||
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email address '" . $email_full . "'");
|
||||
|
||||
$result = $this->apiCall('Emails.get', [
|
||||
@@ -194,7 +221,7 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
$customer_ids = $this->getAllowedCustomerIds('email');
|
||||
$params['idea'] = ($id <= 0 ? $emailaddr : $id);
|
||||
|
||||
$result_stmt = Database::prepare("SELECT v.`id`, v.`email`, v.`email_full`, v.`iscatchall`, v.`destination`, v.`customerid`, v.`popaccountid`, v.`domainid`, v.`description`, u.`quota`, u.`imap`, u.`pop3`, u.`postfix`, u.`mboxsize`
|
||||
$result_stmt = Database::prepare("SELECT v.*, u.`quota`, u.`imap`, u.`pop3`, u.`postfix`, u.`mboxsize`
|
||||
FROM `" . TABLE_MAIL_VIRTUAL . "` v
|
||||
LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON v.`popaccountid` = u.`id`
|
||||
WHERE v.`customerid` IN (" . implode(", ", $customer_ids) . ")
|
||||
@@ -220,6 +247,14 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
* optional, required when called as admin (if $loginname is not specified)
|
||||
* @param string $loginname
|
||||
* optional, required when called as admin (if $customerid is not specified)
|
||||
* @param float $spam_tag_level
|
||||
* optional, score which is required to tag emails as spam, default: 7.0
|
||||
* @param float $spam_kill_level
|
||||
* optional, score which is required to discard emails, default: 14.0
|
||||
* @param boolean $bypass_spam
|
||||
* optional, disable spam-filter entirely, default: no
|
||||
* @param boolean $policy_greylist
|
||||
* optional, enable grey-listing, default: yes
|
||||
* @param boolean $iscatchall
|
||||
* optional
|
||||
* @param string $description
|
||||
@@ -255,6 +290,10 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
$id = $result['id'];
|
||||
|
||||
// parameters
|
||||
$spam_tag_level = $this->getParam('spam_tag_level', true, $result['spam_tag_level']);
|
||||
$spam_kill_level = $this->getParam('spam_kill_level', true, $result['spam_kill_level']);
|
||||
$bypass_spam = $this->getBoolParam('bypass_spam', true, $result['bypass_spam']);
|
||||
$policy_greylist = $this->getBoolParam('policy_greylist', true, $result['policy_greylist']);
|
||||
$iscatchall = $this->getBoolParam('iscatchall', true, $result['iscatchall']);
|
||||
$description = $this->getParam('description', true, $result['description']);
|
||||
|
||||
@@ -284,19 +323,34 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
$email = $result['email_full'];
|
||||
}
|
||||
|
||||
$spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1,2})?$/', '', [7.0], true);
|
||||
$spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1,2})?$/', '', [14.0], true);
|
||||
$description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true);
|
||||
|
||||
$stmt = Database::prepare("
|
||||
UPDATE `" . TABLE_MAIL_VIRTUAL . "`
|
||||
SET `email` = :email , `iscatchall` = :caflag, `description` = :description
|
||||
UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET
|
||||
`email` = :email ,
|
||||
`spam_tag_level` = :spam_tag_level,
|
||||
`spam_kill_level` = :spam_kill_level,
|
||||
`bypass_spam` = :bypass_spam,
|
||||
`policy_greylist` = :policy_greylist,
|
||||
`iscatchall` = :caflag,
|
||||
`description` = :description
|
||||
WHERE `customerid`= :cid AND `id`= :id
|
||||
");
|
||||
$params = [
|
||||
"email" => $email,
|
||||
"spam_tag_level" => $spam_tag_level,
|
||||
"spam_kill_level" => $spam_kill_level,
|
||||
"bypass_spam" => $bypass_spam,
|
||||
"policy_greylist" => $policy_greylist,
|
||||
"caflag" => $iscatchall,
|
||||
"description" => $description,
|
||||
"cid" => $customer['customerid'],
|
||||
"id" => $id
|
||||
];
|
||||
Database::pexecute($stmt, $params, true, true);
|
||||
Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
|
||||
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] toggled catchall-flag for email address '" . $result['email_full'] . "'");
|
||||
|
||||
$result = $this->apiCall('Emails.get', [
|
||||
@@ -334,7 +388,7 @@ class Emails extends ApiCommand implements ResourceEntity
|
||||
$result = [];
|
||||
$query_fields = [];
|
||||
$result_stmt = Database::prepare("
|
||||
SELECT m.`id`, m.`domainid`, m.`email`, m.`email_full`, m.`iscatchall`, m.`destination`, m.`popaccountid`, d.`domain`, u.`quota`, u.`imap`, u.`pop3`, u.`postfix`, u.`mboxsize`
|
||||
SELECT m.*, d.`domain`, u.`quota`, u.`imap`, u.`pop3`, u.`postfix`, u.`mboxsize`
|
||||
FROM `" . TABLE_MAIL_VIRTUAL . "` m
|
||||
LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON (m.`domainid` = d.`id`)
|
||||
LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`)
|
||||
|
||||
@@ -43,9 +43,7 @@ final class ConfigServices extends CliCommand
|
||||
{
|
||||
private $yes_to_all_supported = [
|
||||
'bookworm',
|
||||
'bionic',
|
||||
'bullseye',
|
||||
'buster',
|
||||
'focal',
|
||||
'jammy',
|
||||
];
|
||||
@@ -172,8 +170,8 @@ final class ConfigServices extends CliCommand
|
||||
$distributions_select_data = [];
|
||||
|
||||
//set default os.
|
||||
$os_dist = ['ID' => 'bullseye'];
|
||||
$os_version = ['0' => '11'];
|
||||
$os_dist = ['ID' => 'bookworm'];
|
||||
$os_version = ['0' => '12'];
|
||||
$os_default = $os_dist['ID'];
|
||||
|
||||
//read os-release
|
||||
|
||||
@@ -27,10 +27,13 @@ namespace Froxlor\Cli;
|
||||
|
||||
use Exception;
|
||||
use Froxlor\Config\ConfigParser;
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\Froxlor;
|
||||
use Froxlor\Install\Install;
|
||||
use Froxlor\Install\Install\Core;
|
||||
use Froxlor\Settings;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -50,7 +53,8 @@ final class InstallCommand extends Command
|
||||
$this->setDescription('Installation process to use instead of web-ui');
|
||||
$this->addArgument('input-file', InputArgument::OPTIONAL, 'Optional JSON array file to use for unattended installations');
|
||||
$this->addOption('print-example-file', 'p', InputOption::VALUE_NONE, 'Outputs an example JSON content to be used with the input file parameter')
|
||||
->addOption('create-userdata-from-str', 'c', InputOption::VALUE_REQUIRED, 'Creates lib/userdata.inc.php file from string created by web-install process');
|
||||
->addOption('create-userdata-from-str', 'c', InputOption::VALUE_REQUIRED, 'Creates lib/userdata.inc.php file from string created by web-install process')
|
||||
->addOption('show-sysinfo', 's', InputOption::VALUE_NONE, 'Outputs system information about your froxlor installation');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +76,15 @@ final class InstallCommand extends Command
|
||||
return self::INVALID;
|
||||
}
|
||||
|
||||
if ($input->getOption('show-sysinfo') !== false) {
|
||||
if (!file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) {
|
||||
$output->writeln("<error>Could not find froxlor's userdata.inc.php file. You can use this parameter only with an installed froxlor system.</>");
|
||||
return self::INVALID;
|
||||
}
|
||||
$this->printSysInfo($output);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
session_start();
|
||||
|
||||
require __DIR__ . '/install.functions.php';
|
||||
@@ -349,6 +362,57 @@ final class InstallCommand extends Command
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
private function printSysInfo(OutputInterface $output)
|
||||
{
|
||||
|
||||
$php_sapi = 'mod_php';
|
||||
$php_version = phpversion();
|
||||
if (Settings::Get('system.mod_fcgid') == '1') {
|
||||
$php_sapi = 'FCGID';
|
||||
if (Settings::Get('system.mod_fcgid_ownvhost') == '1') {
|
||||
$php_sapi .= ' (+ froxlor)';
|
||||
}
|
||||
} elseif (Settings::Get('phpfpm.enabled') == '1') {
|
||||
$php_sapi = 'PHP-FPM';
|
||||
if (Settings::Get('phpfpm.enabled_ownvhost') == '1') {
|
||||
$php_sapi .= ' (+ froxlor)';
|
||||
}
|
||||
}
|
||||
|
||||
$kernel = 'unknown';
|
||||
if (function_exists('posix_uname')) {
|
||||
$kernel_nfo = posix_uname();
|
||||
$kernel = $kernel_nfo['release'] . ' (' . $kernel_nfo['machine'] . ')';
|
||||
}
|
||||
|
||||
$ips = [];
|
||||
$ips_stmt = Database::query("SELECT CONCAT(`ip`, ' (', `port`, ')') as ipaddr FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `id`");
|
||||
while ($ip = $ips_stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$ips[] = $ip['ipaddr'];
|
||||
}
|
||||
|
||||
$table = new Table($output);
|
||||
$table
|
||||
->setHeaders([
|
||||
'Key', 'Value'
|
||||
])
|
||||
->setRows([
|
||||
['Froxlor', Froxlor::getVersionString()],
|
||||
['Update-channel', Settings::Get('system.update_channel')],
|
||||
['Hostname', Settings::Get('system.hostname')],
|
||||
['Install-dir', Froxlor::getInstallDir()],
|
||||
['PHP CLI', $php_version],
|
||||
['PHP SAPI', $php_sapi],
|
||||
['Webserver', Settings::Get('system.webserver')],
|
||||
['Kernel', $kernel],
|
||||
['Database', Database::getAttribute(\PDO::ATTR_SERVER_VERSION)],
|
||||
['Distro config', Settings::Get('system.distribution')],
|
||||
['IP addresses', implode("\n", $ips)],
|
||||
]);
|
||||
$table->setStyle('box');
|
||||
$table->render();
|
||||
}
|
||||
|
||||
private function cliTextFormat(string $text, string $nl_char = "\n"): string
|
||||
{
|
||||
$text = str_replace(['<br>', '<br/>', '<br />'], [$nl_char, $nl_char, $nl_char], $text);
|
||||
|
||||
@@ -52,7 +52,7 @@ final class MasterCron extends CliCommand
|
||||
$this->setName('froxlor:cron');
|
||||
$this->setDescription('Regulary perform tasks created by froxlor');
|
||||
$this->addArgument('job', InputArgument::IS_ARRAY, 'Job(s) to run');
|
||||
$this->addOption('run-task', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run a specific task [1 = re-generate configs, 4 = re-generate dns zones, 10 = re-set quotas, 99 = re-create cron.d-file]')
|
||||
$this->addOption('run-task', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run a specific task [1 = re-generate configs, 4 = re-generate dns zones, 9 = re-generate rspamd configs, 10 = re-set quotas, 99 = re-create cron.d-file]')
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces given job or, if none given, forces re-generating of config-files (webserver, nameserver, etc.)')
|
||||
->addOption('debug', 'd', InputOption::VALUE_NONE, 'Output debug information about what is going on to STDOUT.')
|
||||
->addOption('no-fork', 'N', InputOption::VALUE_NONE, 'Do not fork to background (traffic cron only).');
|
||||
@@ -77,6 +77,7 @@ final class MasterCron extends CliCommand
|
||||
if (empty($jobs) || in_array('tasks', $jobs)) {
|
||||
Cronjob::inserttask(TaskId::REBUILD_VHOST);
|
||||
Cronjob::inserttask(TaskId::REBUILD_DNS);
|
||||
Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
|
||||
Cronjob::inserttask(TaskId::CREATE_QUOTA);
|
||||
Cronjob::inserttask(TaskId::REBUILD_CRON);
|
||||
$jobs[] = 'tasks';
|
||||
@@ -95,7 +96,7 @@ final class MasterCron extends CliCommand
|
||||
if ($input->getOption('run-task')) {
|
||||
$tasks_to_run = $input->getOption('run-task');
|
||||
foreach ($tasks_to_run as $ttr) {
|
||||
if (in_array($ttr, [TaskId::REBUILD_VHOST, TaskId::REBUILD_DNS, TaskId::CREATE_QUOTA, TaskId::REBUILD_CRON])) {
|
||||
if (in_array($ttr, [TaskId::REBUILD_VHOST, TaskId::REBUILD_DNS, TaskId::REBUILD_RSPAMD, TaskId::CREATE_QUOTA, TaskId::REBUILD_CRON])) {
|
||||
Cronjob::inserttask($ttr);
|
||||
$jobs[] = 'tasks';
|
||||
} else {
|
||||
|
||||
@@ -117,85 +117,6 @@ abstract class DnsBase
|
||||
}
|
||||
}
|
||||
|
||||
public function writeDKIMconfigs()
|
||||
{
|
||||
if (Settings::Get('dkim.use_dkim') == '1') {
|
||||
if (!file_exists(FileDir::makeCorrectDir(Settings::Get('dkim.dkim_prefix')))) {
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('dkim.dkim_prefix'))));
|
||||
FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('dkim.dkim_prefix'))));
|
||||
}
|
||||
|
||||
$dkimdomains = '';
|
||||
$dkimkeys = '';
|
||||
$result_domains_stmt = Database::query("
|
||||
SELECT `id`, `domain`, `dkim`, `dkim_id`, `dkim_pubkey`, `dkim_privkey`
|
||||
FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `dkim` = '1' ORDER BY `id` ASC
|
||||
");
|
||||
|
||||
while ($domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$privkey_filename = FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . Settings::Get('dkim.privkeysuffix'));
|
||||
$pubkey_filename = FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . '.public');
|
||||
|
||||
if ($domain['dkim_privkey'] == '' || $domain['dkim_pubkey'] == '') {
|
||||
$max_dkim_id_stmt = Database::query("SELECT MAX(`dkim_id`) as `max_dkim_id` FROM `" . TABLE_PANEL_DOMAINS . "`");
|
||||
$max_dkim_id = $max_dkim_id_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$domain['dkim_id'] = (int)$max_dkim_id['max_dkim_id'] + 1;
|
||||
$privkey_filename = FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . Settings::Get('dkim.privkeysuffix'));
|
||||
FileDir::safe_exec('openssl genrsa -out ' . escapeshellarg($privkey_filename) . ' ' . Settings::Get('dkim.dkim_keylength'));
|
||||
$domain['dkim_privkey'] = file_get_contents($privkey_filename);
|
||||
FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename));
|
||||
$pubkey_filename = FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/dkim' . $domain['dkim_id'] . '.public');
|
||||
FileDir::safe_exec('openssl rsa -in ' . escapeshellarg($privkey_filename) . ' -pubout -outform pem -out ' . escapeshellarg($pubkey_filename));
|
||||
$domain['dkim_pubkey'] = file_get_contents($pubkey_filename);
|
||||
FileDir::safe_exec("chmod 0664 " . escapeshellarg($pubkey_filename));
|
||||
$upd_stmt = Database::prepare("
|
||||
UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
|
||||
`dkim_id` = :dkimid,
|
||||
`dkim_privkey` = :privkey,
|
||||
`dkim_pubkey` = :pubkey
|
||||
WHERE `id` = :id
|
||||
");
|
||||
$upd_data = [
|
||||
'dkimid' => $domain['dkim_id'],
|
||||
'privkey' => $domain['dkim_privkey'],
|
||||
'pubkey' => $domain['dkim_pubkey'],
|
||||
'id' => $domain['id']
|
||||
];
|
||||
Database::pexecute($upd_stmt, $upd_data);
|
||||
}
|
||||
|
||||
if (!file_exists($privkey_filename) && $domain['dkim_privkey'] != '') {
|
||||
$privkey_file_handler = fopen($privkey_filename, "w");
|
||||
fwrite($privkey_file_handler, $domain['dkim_privkey']);
|
||||
fclose($privkey_file_handler);
|
||||
FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename));
|
||||
}
|
||||
|
||||
if (!file_exists($pubkey_filename) && $domain['dkim_pubkey'] != '') {
|
||||
$pubkey_file_handler = fopen($pubkey_filename, "w");
|
||||
fwrite($pubkey_file_handler, $domain['dkim_pubkey']);
|
||||
fclose($pubkey_file_handler);
|
||||
FileDir::safe_exec("chmod 0644 " . escapeshellarg($pubkey_filename));
|
||||
}
|
||||
|
||||
$dkimdomains .= $domain['domain'] . "\n";
|
||||
$dkimkeys .= "*@" . $domain['domain'] . ":" . $domain['domain'] . ":" . $privkey_filename . "\n";
|
||||
}
|
||||
|
||||
$dkimdomains_filename = FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/' . Settings::Get('dkim.dkim_domains'));
|
||||
$dkimdomains_file_handler = fopen($dkimdomains_filename, "w");
|
||||
fwrite($dkimdomains_file_handler, $dkimdomains);
|
||||
fclose($dkimdomains_file_handler);
|
||||
$dkimkeys_filename = FileDir::makeCorrectFile(Settings::Get('dkim.dkim_prefix') . '/' . Settings::Get('dkim.dkim_dkimkeys'));
|
||||
$dkimkeys_file_handler = fopen($dkimkeys_filename, "w");
|
||||
fwrite($dkimkeys_file_handler, $dkimkeys);
|
||||
fclose($dkimkeys_file_handler);
|
||||
|
||||
FileDir::safe_exec(escapeshellcmd(Settings::Get('dkim.dkimrestart_command')));
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Dkim-milter reloaded');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDomainList()
|
||||
{
|
||||
$result_domains_stmt = Database::query("
|
||||
|
||||
216
lib/Froxlor/Cron/Mail/Rspamd.php
Normal file
216
lib/Froxlor/Cron/Mail/Rspamd.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
* Copyright (c) 2010 the Froxlor Team (see authors).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, you can also view it online at
|
||||
* https://files.froxlor.org/misc/COPYING.txt
|
||||
*
|
||||
* @copyright the authors
|
||||
* @author Froxlor team <team@froxlor.org>
|
||||
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
|
||||
*/
|
||||
|
||||
namespace Froxlor\Cron\Mail;
|
||||
|
||||
use Exception;
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\FileDir;
|
||||
use Froxlor\FroxlorLogger;
|
||||
use Froxlor\Settings;
|
||||
|
||||
class Rspamd
|
||||
{
|
||||
const DEFAULT_MARK_LVL = 7.0;
|
||||
const DEFAULT_REJECT_LVL = 14.0;
|
||||
|
||||
private string $frx_settings_file = "";
|
||||
|
||||
protected FroxlorLogger $logger;
|
||||
|
||||
public function __construct(FroxlorLogger $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function writeConfigs()
|
||||
{
|
||||
// tell the world what we are doing
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task9 started - Rebuilding antispam configuration');
|
||||
|
||||
// get all email addresses
|
||||
$antispam_stmt = Database::prepare("
|
||||
SELECT email, spam_tag_level, spam_kill_level, bypass_spam, policy_greylist, iscatchall
|
||||
FROM `" . TABLE_MAIL_VIRTUAL . "`
|
||||
ORDER BY email
|
||||
");
|
||||
Database::pexecute($antispam_stmt);
|
||||
|
||||
$this->frx_settings_file = "#\n# Automatically generated file by froxlor. DO NOT EDIT manually as it will be overwritten!\n# Generated: " . date('d.m.Y H:i') . "\n#\n\n";
|
||||
while ($email = $antispam_stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$this->generateEmailAddrConfig($email);
|
||||
}
|
||||
|
||||
$antispam_cfg_file = FileDir::makeCorrectFile(Settings::Get('antispam.config_file'));
|
||||
file_put_contents($antispam_cfg_file, $this->frx_settings_file);
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, $antispam_cfg_file . ' written');
|
||||
$this->writeDkimConfigs();
|
||||
$this->reloadDaemon();
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task9 finished');
|
||||
}
|
||||
|
||||
/**
|
||||
* # local.d/dkim_signing.conf
|
||||
* try_fallback = true;
|
||||
* path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
* selector_map = "/etc/rspamd/dkim_selectors.map";
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function writeDkimConfigs()
|
||||
{
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Writing DKIM key-pairs');
|
||||
|
||||
$dkim_selector_map = "";
|
||||
$result_domains_stmt = Database::query("
|
||||
SELECT `id`, `domain`, `dkim`, `dkim_id`, `dkim_pubkey`, `dkim_privkey`
|
||||
FROM `" . TABLE_PANEL_DOMAINS . "`
|
||||
WHERE `dkim` = '1'
|
||||
ORDER BY `id` ASC
|
||||
");
|
||||
|
||||
while ($domain = $result_domains_stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
|
||||
if ($domain['dkim_privkey'] == '' || $domain['dkim_pubkey'] == '') {
|
||||
$max_dkim_id_stmt = Database::query("SELECT MAX(`dkim_id`) as `max_dkim_id` FROM `" . TABLE_PANEL_DOMAINS . "`");
|
||||
$max_dkim_id = $max_dkim_id_stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$domain['dkim_id'] = (int)$max_dkim_id['max_dkim_id'] + 1;
|
||||
|
||||
$privkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.key');
|
||||
$pubkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.txt');
|
||||
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Generating DKIM keys for "' . $domain['domain'] . '"');
|
||||
$rsret = [];
|
||||
FileDir::safe_exec(
|
||||
'rspamadm dkim_keygen -d ' . escapeshellarg($domain['domain']) . ' -k ' . $privkey_filename . ' -s dkim' . $domain['dkim_id'] . ' -b ' . Settings::Get('dkim.dkim_keylength') . ' -o plain > ' . escapeshellarg($pubkey_filename),
|
||||
$rsret,
|
||||
['>']
|
||||
);
|
||||
if (!file_exists($privkey_filename) || !file_exists($pubkey_filename)) {
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'DKIM Keypair for domain "' . $domain['domain'] . '" was not generated successfully.');
|
||||
continue;
|
||||
}
|
||||
$domain['dkim_privkey'] = file_get_contents($privkey_filename);
|
||||
FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename));
|
||||
FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($privkey_filename));
|
||||
$domain['dkim_pubkey'] = file_get_contents($pubkey_filename);
|
||||
FileDir::safe_exec("chmod 0664 " . escapeshellarg($pubkey_filename));
|
||||
FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($pubkey_filename));
|
||||
$upd_stmt = Database::prepare("
|
||||
UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
|
||||
`dkim_id` = :dkimid,
|
||||
`dkim_privkey` = :privkey,
|
||||
`dkim_pubkey` = :pubkey
|
||||
WHERE `id` = :id
|
||||
");
|
||||
$upd_data = [
|
||||
'dkimid' => $domain['dkim_id'],
|
||||
'privkey' => $domain['dkim_privkey'],
|
||||
'pubkey' => $domain['dkim_pubkey'],
|
||||
'id' => $domain['id']
|
||||
];
|
||||
Database::pexecute($upd_stmt, $upd_data);
|
||||
} else {
|
||||
$privkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.key');
|
||||
$pubkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.txt');
|
||||
}
|
||||
|
||||
if (!file_exists($privkey_filename) && $domain['dkim_privkey'] != '') {
|
||||
file_put_contents($privkey_filename, $domain['dkim_privkey']);
|
||||
FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename));
|
||||
FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($privkey_filename));
|
||||
}
|
||||
|
||||
if (!file_exists($pubkey_filename) && $domain['dkim_pubkey'] != '') {
|
||||
file_put_contents($pubkey_filename, $domain['dkim_pubkey']);
|
||||
FileDir::safe_exec("chmod 0644 " . escapeshellarg($pubkey_filename));
|
||||
FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($pubkey_filename));
|
||||
}
|
||||
|
||||
$dkim_selector_map .= $domain['domain'] . " dkim" . $domain['dkim_id'] . "\n";
|
||||
}
|
||||
|
||||
$dkim_selector_file = FileDir::makeCorrectFile('/etc/rspamd/dkim_selectors.map');
|
||||
file_put_contents($dkim_selector_file, $dkim_selector_map);
|
||||
}
|
||||
|
||||
private function generateEmailAddrConfig(array $email): void
|
||||
{
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Generating antispam config for ' . $email['email']);
|
||||
|
||||
$email['spam_tag_level'] = floatval($email['spam_tag_level']);
|
||||
$email['spam_kill_level'] = floatval($email['spam_kill_level']);
|
||||
$email_id = md5($email['email']);
|
||||
|
||||
$this->frx_settings_file .= '# Email: ' . $email['email'] . "\n";
|
||||
foreach (['rcpt', 'from'] as $type) {
|
||||
$this->frx_settings_file .= 'frx_' . $email_id . '_' . $type . ' {' . "\n";
|
||||
$this->frx_settings_file .= ' id = "frx_' . $email_id . '_' . $type . '";' . "\n";
|
||||
if ($email['iscatchall']) {
|
||||
$this->frx_settings_file .= ' priority = low;' . "\n";
|
||||
$this->frx_settings_file .= ' ' . $type . ' = "' . substr($email['email'], strpos($email['email'], '@')) . '";' . "\n";
|
||||
} else {
|
||||
$this->frx_settings_file .= ' priority = medium;' . "\n";
|
||||
$this->frx_settings_file .= ' ' . $type . ' = "' . $email['email'] . '";' . "\n";
|
||||
}
|
||||
if ((int)$email['bypass_spam'] == 1) {
|
||||
$this->frx_settings_file .= ' want_spam = yes;' . "\n";
|
||||
} else {
|
||||
$this->frx_settings_file .= ' apply {' . "\n";
|
||||
$this->frx_settings_file .= ' actions {' . "\n";
|
||||
$this->frx_settings_file .= ' "add header" = ' . $email['spam_tag_level'] . ';' . "\n";
|
||||
$this->frx_settings_file .= ' rewrite_subject = ' . $email['spam_tag_level'] . ';' . "\n";
|
||||
$this->frx_settings_file .= ' reject = ' . $email['spam_kill_level'] . ';' . "\n";
|
||||
if ($type == 'rcpt' && (int)$email['policy_greylist'] == 0) {
|
||||
$this->frx_settings_file .= ' greylist = null;' . "\n";
|
||||
}
|
||||
$this->frx_settings_file .= ' }' . "\n";
|
||||
$this->frx_settings_file .= ' }' . "\n";
|
||||
if ($type == 'rcpt' && (int)$email['policy_greylist'] == 0) {
|
||||
$this->frx_settings_file .= ' symbols [ "DONT_GREYLIST" ]' . "\n";
|
||||
}
|
||||
}
|
||||
$this->frx_settings_file .= '}' . "\n";
|
||||
}
|
||||
$this->frx_settings_file .= "\n";
|
||||
}
|
||||
|
||||
public function reloadDaemon()
|
||||
{
|
||||
// reload DNS daemon
|
||||
$cmd = Settings::Get('antispam.reload_command');
|
||||
$cmdStatus = 1;
|
||||
FileDir::safe_exec(escapeshellcmd($cmd), $cmdStatus);
|
||||
if ($cmdStatus === 0) {
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Antispam daemon reloaded');
|
||||
} else {
|
||||
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error while running `' . $cmd . '`: exit code (' . $cmdStatus . ') - please check your system logs');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,11 @@
|
||||
|
||||
namespace Froxlor\Cron\System;
|
||||
|
||||
use Exception;
|
||||
use Froxlor\Cron\FroxlorCron;
|
||||
use Froxlor\Cron\Http\ConfigIO;
|
||||
use Froxlor\Cron\Http\HttpConfigBase;
|
||||
use Froxlor\Cron\Mail\Rspamd;
|
||||
use Froxlor\Cron\TaskId;
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\Dns\PowerDNS;
|
||||
@@ -40,6 +42,9 @@ use PDO;
|
||||
class TasksCron extends FroxlorCron
|
||||
{
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function run()
|
||||
{
|
||||
/**
|
||||
@@ -98,6 +103,11 @@ class TasksCron extends FroxlorCron
|
||||
* refs #293
|
||||
*/
|
||||
self::deleteFtpData($row);
|
||||
} elseif ($row['type'] == TaskId::REBUILD_RSPAMD && (int)Settings::Get('antispam.activated') != 0) {
|
||||
/**
|
||||
* TYPE=9 Rebuild antispam config
|
||||
*/
|
||||
self::rebuildAntiSpamConfigs();
|
||||
} elseif ($row['type'] == TaskId::CREATE_QUOTA && (int)Settings::Get('system.diskquota_enabled') != 0) {
|
||||
/**
|
||||
* TYPE=10 Set the filesystem - quota
|
||||
@@ -266,13 +276,7 @@ class TasksCron extends FroxlorCron
|
||||
private static function rebuildDnsConfigs()
|
||||
{
|
||||
$dnssrv = '\\Froxlor\\Cron\\Dns\\' . Settings::Get('system.dns_server');
|
||||
|
||||
$nameserver = new $dnssrv(FroxlorLogger::getInstanceOf());
|
||||
|
||||
if (Settings::Get('dkim.use_dkim') == '1') {
|
||||
$nameserver->writeDKIMconfigs();
|
||||
}
|
||||
|
||||
$nameserver->writeConfigs();
|
||||
}
|
||||
|
||||
@@ -448,4 +452,13 @@ class TasksCron extends FroxlorCron
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function rebuildAntiSpamConfigs()
|
||||
{
|
||||
$antispam = new Rspamd(FroxlorLogger::getInstanceOf());
|
||||
$antispam->writeConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,12 @@ final class TaskId
|
||||
*/
|
||||
const DELETE_FTP_DATA = 8;
|
||||
|
||||
/**
|
||||
* TYPE=9 MEANS THAT SOMETHING ANTISPAM RELATED HAS CHANGED.
|
||||
* REBUILD froxlor_settings.conf IF ANTISPAM IS ENABLED
|
||||
*/
|
||||
const REBUILD_RSPAMD = 9;
|
||||
|
||||
/**
|
||||
* TYPE=10 Set the filesystem - quota
|
||||
*/
|
||||
|
||||
@@ -32,7 +32,7 @@ class Customer
|
||||
{
|
||||
|
||||
/**
|
||||
* Get value of a a specific field from a given customer
|
||||
* Get value of a specific field from a given customer
|
||||
*
|
||||
* @param int $customerid
|
||||
* @param string $varname
|
||||
|
||||
@@ -120,16 +120,14 @@ class Dns
|
||||
if ($domain['isemaildomain'] == '1') {
|
||||
self::addRequiredEntry('@', 'MX', $required_entries);
|
||||
if (Settings::Get('system.dns_createmailentry')) {
|
||||
foreach (
|
||||
[
|
||||
foreach ([
|
||||
'imap',
|
||||
'pop3',
|
||||
'mail',
|
||||
'smtp'
|
||||
] as $record
|
||||
) {
|
||||
foreach (
|
||||
[
|
||||
foreach ([
|
||||
'AAAA',
|
||||
'A'
|
||||
] as $type
|
||||
@@ -152,9 +150,9 @@ class Dns
|
||||
if (!$froxlorhostname) {
|
||||
// additional required records for subdomains
|
||||
$subdomains_stmt = Database::prepare("
|
||||
SELECT `domain`, `iswildcarddomain`, `wwwserveralias`, `isemaildomain` FROM `" . TABLE_PANEL_DOMAINS . "`
|
||||
WHERE `parentdomainid` = :domainid
|
||||
");
|
||||
SELECT `domain`, `iswildcarddomain`, `wwwserveralias`, `isemaildomain` FROM `" . TABLE_PANEL_DOMAINS . "`
|
||||
WHERE `parentdomainid` = :domainid
|
||||
");
|
||||
Database::pexecute($subdomains_stmt, [
|
||||
'domainid' => $domain_id
|
||||
]);
|
||||
@@ -163,7 +161,7 @@ class Dns
|
||||
$sub_record = str_replace('.' . $domain['domain'], '', $subdomain['domain']);
|
||||
// Listing domains is enough as there currently is no support for choosing
|
||||
// different ips for a subdomain => use same IPs as toplevel
|
||||
self::addRequiredEntry($sub_record, 'A',$required_entries);
|
||||
self::addRequiredEntry($sub_record, 'A', $required_entries);
|
||||
self::addRequiredEntry($sub_record, 'AAAA', $required_entries);
|
||||
|
||||
// Check whether to add a www.-prefix
|
||||
@@ -181,7 +179,7 @@ class Dns
|
||||
// check for SPF content later
|
||||
self::addRequiredEntry('@SPF@.' . $sub_record, 'TXT', $required_entries);
|
||||
}
|
||||
if (Settings::Get('dkim.use_dkim') == '1') {
|
||||
if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1') {
|
||||
// check for DKIM content later
|
||||
self::addRequiredEntry('dkim' . $domain['dkim_id'] . '._domainkey.' . $sub_record, 'TXT', $required_entries);
|
||||
}
|
||||
@@ -218,7 +216,7 @@ class Dns
|
||||
// check for SPF content later
|
||||
self::addRequiredEntry('@SPF@', 'TXT', $required_entries);
|
||||
}
|
||||
if (Settings::Get('dkim.use_dkim') == '1') {
|
||||
if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1') {
|
||||
// check for DKIM content later
|
||||
self::addRequiredEntry('dkim' . $domain['dkim_id'] . '._domainkey', 'TXT', $required_entries);
|
||||
}
|
||||
@@ -229,17 +227,25 @@ class Dns
|
||||
|
||||
// now generate all records and unset the required entries we have
|
||||
foreach ($dom_entries as $entry) {
|
||||
if (array_key_exists($entry['type'], $required_entries) && array_key_exists(md5($entry['record']),
|
||||
$required_entries[$entry['type']])) {
|
||||
if (array_key_exists($entry['type'], $required_entries) && array_key_exists(
|
||||
md5($entry['record']),
|
||||
$required_entries[$entry['type']]
|
||||
)) {
|
||||
unset($required_entries[$entry['type']][md5($entry['record'])]);
|
||||
}
|
||||
if (Settings::Get('system.dns_createcaaentry') == '1' && $entry['type'] == 'CAA' && strtolower(substr($entry['content'],
|
||||
0, 7)) == '"v=caa1') {
|
||||
if (Settings::Get('system.dns_createcaaentry') == '1' && $entry['type'] == 'CAA' && strtolower(substr(
|
||||
$entry['content'],
|
||||
0,
|
||||
7
|
||||
)) == '"v=caa1') {
|
||||
// unset special CAA required-entry
|
||||
unset($required_entries[$entry['type']][md5("@CAA@")]);
|
||||
}
|
||||
if (Settings::Get('spf.use_spf') == '1' && $entry['type'] == 'TXT' && $entry['record'] == '@' && (strtolower(substr($entry['content'],
|
||||
0, 7)) == '"v=spf1' || strtolower(substr($entry['content'], 0, 6)) == 'v=spf1')) {
|
||||
if (Settings::Get('spf.use_spf') == '1' && $entry['type'] == 'TXT' && $entry['record'] == '@' && (strtolower(substr(
|
||||
$entry['content'],
|
||||
0,
|
||||
7
|
||||
)) == '"v=spf1' || strtolower(substr($entry['content'], 0, 6)) == 'v=spf1')) {
|
||||
// unset special spf required-entry
|
||||
unset($required_entries[$entry['type']][md5("@SPF@")]);
|
||||
}
|
||||
@@ -248,32 +254,36 @@ class Dns
|
||||
$primary_ns = $entry['content'];
|
||||
}
|
||||
// check for CNAME on @, www- or wildcard-Alias and remove A/AAAA record accordingly
|
||||
foreach (
|
||||
[
|
||||
foreach ([
|
||||
'@',
|
||||
'www',
|
||||
'*'
|
||||
] as $crecord
|
||||
) {
|
||||
if ($entry['type'] == 'CNAME' && $entry['record'] == '@' && (array_key_exists(md5($crecord),
|
||||
$required_entries['A']) || array_key_exists(md5($crecord), $required_entries['AAAA']))) {
|
||||
if ($entry['type'] == 'CNAME' && $entry['record'] == '@' && (array_key_exists(
|
||||
md5($crecord),
|
||||
$required_entries['A']
|
||||
) || array_key_exists(md5($crecord), $required_entries['AAAA']))) {
|
||||
unset($required_entries['A'][md5($crecord)]);
|
||||
unset($required_entries['AAAA'][md5($crecord)]);
|
||||
}
|
||||
}
|
||||
// also allow overriding of auto-generated values (imap,pop3,mail,smtp) if enabled in the settings
|
||||
if (Settings::Get('system.dns_createmailentry')) {
|
||||
foreach (
|
||||
[
|
||||
foreach ([
|
||||
'imap',
|
||||
'pop3',
|
||||
'mail',
|
||||
'smtp'
|
||||
] as $crecord
|
||||
) {
|
||||
if ($entry['type'] == 'CNAME' && $entry['record'] == $crecord && (array_key_exists(md5($crecord),
|
||||
$required_entries['A']) || array_key_exists(md5($crecord),
|
||||
$required_entries['AAAA']))) {
|
||||
if ($entry['type'] == 'CNAME' && $entry['record'] == $crecord && (array_key_exists(
|
||||
md5($crecord),
|
||||
$required_entries['A']
|
||||
) || array_key_exists(
|
||||
md5($crecord),
|
||||
$required_entries['AAAA']
|
||||
))) {
|
||||
unset($required_entries['A'][md5($crecord)]);
|
||||
unset($required_entries['AAAA'][md5($crecord)]);
|
||||
}
|
||||
@@ -310,8 +320,11 @@ class Dns
|
||||
foreach ($records as $record) {
|
||||
if ($type == 'A' && filter_var($ip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
|
||||
$zonerecords[] = new DnsEntry($record, 'A', $ip['ip']);
|
||||
} elseif ($type == 'AAAA' && filter_var($ip['ip'], FILTER_VALIDATE_IP,
|
||||
FILTER_FLAG_IPV6) !== false) {
|
||||
} elseif ($type == 'AAAA' && filter_var(
|
||||
$ip['ip'],
|
||||
FILTER_VALIDATE_IP,
|
||||
FILTER_FLAG_IPV6
|
||||
) !== false) {
|
||||
$zonerecords[] = new DnsEntry($record, 'AAAA', $ip['ip']);
|
||||
}
|
||||
}
|
||||
@@ -376,9 +389,7 @@ class Dns
|
||||
|
||||
// TXT (SPF and DKIM)
|
||||
if (array_key_exists("TXT", $required_entries)) {
|
||||
if (Settings::Get('dkim.use_dkim') == '1') {
|
||||
$dkim_entries = self::generateDkimEntries($domain);
|
||||
}
|
||||
$dkim_entries = self::generateDkimEntries($domain);
|
||||
|
||||
foreach ($required_entries as $type => $records) {
|
||||
if ($type == 'TXT') {
|
||||
@@ -471,8 +482,10 @@ class Dns
|
||||
|
||||
if (!$isMainButSubTo) {
|
||||
$date = date('Ymd');
|
||||
$domain['bindserial'] = (preg_match('/^' . $date . '/',
|
||||
$domain['bindserial']) ? $domain['bindserial'] + 1 : $date . '00');
|
||||
$domain['bindserial'] = (preg_match(
|
||||
'/^' . $date . '/',
|
||||
$domain['bindserial']
|
||||
) ? $domain['bindserial'] + 1 : $date . '00');
|
||||
if (!$froxlorhostname) {
|
||||
$upd_stmt = Database::prepare("
|
||||
UPDATE `" . TABLE_PANEL_DOMAINS . "` SET
|
||||
@@ -499,8 +512,12 @@ class Dns
|
||||
array_unshift($zonerecords, $soa_record);
|
||||
}
|
||||
|
||||
$zone = new DnsZone((int)Settings::Get('system.defaultttl'), $domain['domain'], $domain['bindserial'],
|
||||
$zonerecords);
|
||||
$zone = new DnsZone(
|
||||
(int)Settings::Get('system.defaultttl'),
|
||||
$domain['domain'],
|
||||
$domain['bindserial'],
|
||||
$zonerecords
|
||||
);
|
||||
|
||||
return $zone;
|
||||
}
|
||||
@@ -527,43 +544,11 @@ class Dns
|
||||
{
|
||||
$zone_dkim = [];
|
||||
|
||||
if (Settings::Get('dkim.use_dkim') == '1' && $domain['dkim'] == '1' && $domain['dkim_pubkey'] != '') {
|
||||
if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1' && $domain['dkim_pubkey'] != '') {
|
||||
// start
|
||||
$dkim_txt = 'v=DKIM1;';
|
||||
|
||||
// algorithm
|
||||
$algorithm = explode(',', Settings::Get('dkim.dkim_algorithm'));
|
||||
$alg = '';
|
||||
foreach ($algorithm as $a) {
|
||||
if ($a == 'all') {
|
||||
break;
|
||||
} else {
|
||||
$alg .= $a . ':';
|
||||
}
|
||||
}
|
||||
|
||||
if ($alg != '') {
|
||||
$alg = substr($alg, 0, -1);
|
||||
$dkim_txt .= 'h=' . $alg . ';';
|
||||
}
|
||||
|
||||
// notes
|
||||
if (trim(Settings::Get('dkim.dkim_notes') != '')) {
|
||||
$dkim_txt .= 'n=' . trim(Settings::Get('dkim.dkim_notes')) . ';';
|
||||
}
|
||||
|
||||
// key
|
||||
$dkim_txt .= 'k=rsa;p=' . trim(preg_replace('/-----BEGIN PUBLIC KEY-----(.+)-----END PUBLIC KEY-----/s',
|
||||
'$1', str_replace("\n", '', $domain['dkim_pubkey']))) . ';';
|
||||
|
||||
// service-type
|
||||
if (Settings::Get('dkim.dkim_servicetype') == '1') {
|
||||
$dkim_txt .= 's=email;';
|
||||
}
|
||||
|
||||
// end-part
|
||||
$dkim_txt .= 't=s';
|
||||
|
||||
$dkim_txt .= 'k=rsa;p=' . trim($domain['dkim_pubkey']) . ';';
|
||||
// dkim-entry
|
||||
$zone_dkim[] = $dkim_txt;
|
||||
}
|
||||
|
||||
77
lib/Froxlor/ErrorBag.php
Normal file
77
lib/Froxlor/ErrorBag.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
* Copyright (c) 2010 the Froxlor Team (see authors).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, you can also view it online at
|
||||
* https://files.froxlor.org/misc/COPYING.txt
|
||||
*
|
||||
* @copyright the authors
|
||||
* @author Froxlor team <team@froxlor.org>
|
||||
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
|
||||
*/
|
||||
|
||||
namespace Froxlor;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class to manage the current user / session
|
||||
*/
|
||||
class ErrorBag
|
||||
{
|
||||
|
||||
/**
|
||||
* returns whether there are errors stored
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasErrors(): bool
|
||||
{
|
||||
return !empty($_SESSION) && !empty($_SESSION['_errors']);
|
||||
}
|
||||
|
||||
/**
|
||||
* add error
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function addError(string $data): void
|
||||
{
|
||||
if (!is_array($_SESSION['_errors'])) {
|
||||
$_SESSION['_errors'] = [];
|
||||
}
|
||||
$_SESSION['_errors'][] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return errors and clear session
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getErrors(): array
|
||||
{
|
||||
$errors = $_SESSION['_errors'] ?? [];
|
||||
unset($_SESSION['_errors']);
|
||||
if (Settings::Config('display_php_errors')) {
|
||||
return $errors;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,15 +31,15 @@ final class Froxlor
|
||||
{
|
||||
|
||||
// Main version variable
|
||||
const VERSION = '2.1.4';
|
||||
const VERSION = '2.2.0-dev1';
|
||||
|
||||
// Database version (YYYYMMDDC where C is a daily counter)
|
||||
const DBVERSION = '202312120';
|
||||
const DBVERSION = '202312230';
|
||||
|
||||
// Distribution branding-tag (used for Debian etc.)
|
||||
const BRANDING = '';
|
||||
|
||||
const DOCS_URL = 'https://docs.froxlor.org/v2.1/';
|
||||
const DOCS_URL = 'https://docs.froxlor.org/v2.2/';
|
||||
|
||||
/**
|
||||
* return path to where froxlor is installed, e.g.
|
||||
|
||||
@@ -674,6 +674,7 @@ class Core
|
||||
'http' => $this->validatedData['webserver'],
|
||||
'smtp' => 'postfix_dovecot',
|
||||
'mail' => 'dovecot_postfix2',
|
||||
'antispam' => 'rspamd',
|
||||
'ftp' => 'proftpd',
|
||||
'system' => $system_params
|
||||
];
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
namespace Froxlor\Install;
|
||||
|
||||
use Froxlor\FileDir;
|
||||
use Froxlor\Froxlor;
|
||||
use Froxlor\FroxlorLogger;
|
||||
use Froxlor\Settings;
|
||||
@@ -85,7 +86,7 @@ class Update
|
||||
self::$update_tasks[self::$task_counter]['result'] = 1;
|
||||
break;
|
||||
default:
|
||||
self::$update_tasks[self::$task_counter]['result'] = -1;
|
||||
self::$update_tasks[self::$task_counter]['result'] = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -136,4 +137,36 @@ class Update
|
||||
{
|
||||
return self::$task_counter;
|
||||
}
|
||||
|
||||
public static function cleanOldFiles(array $to_clean)
|
||||
{
|
||||
self::showUpdateStep("Cleaning up old files");
|
||||
$disabled = explode(',', ini_get('disable_functions'));
|
||||
$exec_allowed = !in_array('exec', $disabled);
|
||||
$del_list = "";
|
||||
foreach ($to_clean as $filedir) {
|
||||
$complete_filedir = Froxlor::getInstallDir() . $filedir;
|
||||
if (file_exists($complete_filedir)) {
|
||||
if ($exec_allowed) {
|
||||
FileDir::safe_exec("rm -rf " . escapeshellarg($complete_filedir));
|
||||
} else {
|
||||
$del_list .= "rm -rf " . escapeshellarg($complete_filedir) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($exec_allowed) {
|
||||
self::lastStepStatus(0);
|
||||
} else {
|
||||
if (empty($del_list)) {
|
||||
// none of the files existed
|
||||
self::lastStepStatus(0);
|
||||
} else {
|
||||
self::lastStepStatus(
|
||||
1,
|
||||
'manual commands needed',
|
||||
'Please run the following commands manually:<br><pre>' . $del_list . '</pre>'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,27 +34,6 @@ use voku\helper\AntiXSS;
|
||||
|
||||
class PhpHelper
|
||||
{
|
||||
private static $sort_key = 'id';
|
||||
private static $sort_type = SORT_STRING;
|
||||
|
||||
/**
|
||||
* sort an array by either natural or string sort and a given index where the value for comparison is found
|
||||
*
|
||||
* @param array $list
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function sortListBy(array &$list, string $key = 'id'): bool
|
||||
{
|
||||
self::$sort_type = Settings::Get('panel.natsorting') == 1 ? SORT_NATURAL : SORT_STRING;
|
||||
self::$sort_key = $key;
|
||||
return usort($list, [
|
||||
'self',
|
||||
'sortListByGivenKey'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around htmlentities to handle arrays, with the advantage that you
|
||||
* can select which fields should be handled by htmlentities
|
||||
@@ -101,35 +80,6 @@ class PhpHelper
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces Strings in an array, with the advantage that you
|
||||
* can select which fields should be str_replace'd
|
||||
*
|
||||
* @param string|array $search String or array of strings to search for
|
||||
* @param string|array $replace String or array to replace with
|
||||
* @param string|array $subject String or array The subject array
|
||||
* @param string|array $fields string The fields which should be checked for, separated by spaces
|
||||
*
|
||||
* @return string|array The str_replace'd array
|
||||
*/
|
||||
public static function strReplaceArray($search, $replace, $subject, $fields = '')
|
||||
{
|
||||
if (is_array($subject)) {
|
||||
if (!is_array($fields)) {
|
||||
$fields = self::arrayTrim(explode(' ', $fields));
|
||||
}
|
||||
foreach ($subject as $field => $value) {
|
||||
if ((!is_array($fields) || empty($fields)) || (in_array($field, $fields))) {
|
||||
$subject[$field] = str_replace($search, $replace, $value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$subject = str_replace($search, $replace, $subject);
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* froxlor php error handler
|
||||
*
|
||||
@@ -170,9 +120,8 @@ class PhpHelper
|
||||
$err_display .= '</pre></p>';
|
||||
// end later
|
||||
$err_display .= '</div>';
|
||||
// check for more existing errors
|
||||
$errors = isset(UI::twig()->getGlobals()['global_errors']) ? UI::twig()->getGlobals()['global_errors'] : "";
|
||||
UI::twig()->addGlobal('global_errors', $errors . $err_display);
|
||||
// set errors to session
|
||||
ErrorBag::addError($err_display);
|
||||
// return true to ignore php standard error-handler
|
||||
return true;
|
||||
}
|
||||
@@ -338,7 +287,8 @@ class PhpHelper
|
||||
?string $max = '',
|
||||
string $system = 'si',
|
||||
string $retstring = '%01.2f %s'
|
||||
): string {
|
||||
): string
|
||||
{
|
||||
// Pick units
|
||||
$systems = [
|
||||
'si' => [
|
||||
@@ -421,7 +371,8 @@ class PhpHelper
|
||||
array $haystack,
|
||||
array &$keys = [],
|
||||
string $currentKey = ''
|
||||
): bool {
|
||||
): bool
|
||||
{
|
||||
foreach ($haystack as $key => $value) {
|
||||
$pathkey = empty($currentKey) ? $key : $currentKey . '.' . $key;
|
||||
if (is_array($value)) {
|
||||
@@ -476,19 +427,6 @@ class PhpHelper
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $a
|
||||
* @param array $b
|
||||
* @return int
|
||||
*/
|
||||
private static function sortListByGivenKey(array $a, array $b): int
|
||||
{
|
||||
if (self::$sort_type == SORT_NATURAL) {
|
||||
return strnatcasecmp($a[self::$sort_key], $b[self::$sort_key]);
|
||||
}
|
||||
return strcasecmp($a[self::$sort_key], $b[self::$sort_key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate php file from array.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
namespace Froxlor;
|
||||
|
||||
use Exception;
|
||||
use Froxlor\Database\Database;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
@@ -131,6 +132,7 @@ class Settings
|
||||
self::$conf = [
|
||||
'enable_webupdate' => false,
|
||||
'disable_otp_security_check' => false,
|
||||
'display_php_errors' => false,
|
||||
];
|
||||
|
||||
$configfile = Froxlor::getInstallDir() . '/lib/config.inc.php';
|
||||
@@ -330,7 +332,7 @@ class Settings
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAll() : array
|
||||
public static function getAll(): array
|
||||
{
|
||||
self::init();
|
||||
return self::$data;
|
||||
@@ -338,17 +340,14 @@ class Settings
|
||||
|
||||
/**
|
||||
* get value from config by identifier
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function Config(string $config)
|
||||
{
|
||||
self::init();
|
||||
$sstr = explode(".", $config);
|
||||
$result = self::$conf;
|
||||
foreach ($sstr as $key) {
|
||||
$result = $result[$key] ?? null;
|
||||
if (empty($result)) {
|
||||
break;
|
||||
}
|
||||
$result = self::$conf[$config] ?? null;
|
||||
if (is_null($result)) {
|
||||
throw new Exception('Unknown local config name "' . $config . '"');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -225,6 +225,17 @@ class Store
|
||||
return $returnvalue;
|
||||
}
|
||||
|
||||
public static function storeSettingFieldInsertAntispamTask($fieldname, $fielddata, $newfieldvalue)
|
||||
{
|
||||
// first save the setting itself
|
||||
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);
|
||||
|
||||
if ($returnvalue !== false) {
|
||||
Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
|
||||
}
|
||||
return $returnvalue;
|
||||
}
|
||||
|
||||
public static function storeSettingHostname($fieldname, $fielddata, $newfieldvalue)
|
||||
{
|
||||
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);
|
||||
|
||||
@@ -134,11 +134,15 @@ class Cronjob
|
||||
INSERT INTO `" . TABLE_PANEL_TASKS . "` SET `type` = :type, `data` = :data
|
||||
");
|
||||
|
||||
if ($type == TaskId::REBUILD_VHOST || $type == TaskId::REBUILD_DNS || $type == TaskId::CREATE_FTP || $type == TaskId::CREATE_QUOTA || $type == TaskId::REBUILD_CRON) {
|
||||
if ($type == TaskId::REBUILD_VHOST || $type == TaskId::REBUILD_DNS || $type == TaskId::CREATE_FTP || $type == TaskId::REBUILD_RSPAMD || $type == TaskId::CREATE_QUOTA || $type == TaskId::REBUILD_CRON) {
|
||||
// 4 = bind -> if bind disabled -> no task
|
||||
if ($type == TaskId::REBUILD_DNS && Settings::Get('system.bind_enable') == '0') {
|
||||
return;
|
||||
}
|
||||
// 9 = rspamd -> if antispam disabled -> no task
|
||||
if ($type == TaskId::REBUILD_RSPAMD && Settings::Get('antispam.activated') == '0') {
|
||||
return;
|
||||
}
|
||||
// 10 = quota -> if quota disabled -> no task
|
||||
if ($type == TaskId::CREATE_QUOTA && Settings::Get('system.diskquota_enabled') == '0') {
|
||||
return;
|
||||
|
||||
@@ -42,7 +42,7 @@ class Traffic
|
||||
{
|
||||
$trafficCollectionObj = (new Collection(TrafficAPI::class, $userinfo,
|
||||
self::getParamsByRange($range, ['customer_traffic' => true])));
|
||||
if ($userinfo['adminsession'] == 1) {
|
||||
if (($userinfo['adminsession'] ?? 0) == 1) {
|
||||
$trafficCollectionObj->has('customer', Customers::class, 'customerid', 'customerid');
|
||||
}
|
||||
$trafficCollection = $trafficCollectionObj->get();
|
||||
@@ -58,8 +58,17 @@ class Traffic
|
||||
$mail = $item['mail'];
|
||||
$total = $http + $ftp + $mail;
|
||||
|
||||
if (empty($users[$item['customerid']])) {
|
||||
$users[$item['customerid']] = [
|
||||
'total' => 0.00,
|
||||
'http' => 0.00,
|
||||
'ftp' => 0.00,
|
||||
'mail' => 0.00,
|
||||
];
|
||||
}
|
||||
|
||||
// per user total
|
||||
if ($userinfo['adminsession'] == 1) {
|
||||
if (($userinfo['adminsession'] ?? 0) == 1) {
|
||||
$users[$item['customerid']]['loginname'] = $item['customer']['loginname'];
|
||||
}
|
||||
$users[$item['customerid']]['total'] += $total;
|
||||
@@ -67,6 +76,30 @@ class Traffic
|
||||
$users[$item['customerid']]['ftp'] += $ftp;
|
||||
$users[$item['customerid']]['mail'] += $mail;
|
||||
if (!$overview) {
|
||||
if (empty($years[$item['year']])) {
|
||||
$years[$item['year']] = [
|
||||
'total' => 0.00,
|
||||
'http' => 0.00,
|
||||
'ftp' => 0.00,
|
||||
'mail' => 0.00,
|
||||
];
|
||||
}
|
||||
if (empty($months[$item['month'] . '/' . $item['year']])) {
|
||||
$months[$item['month'] . '/' . $item['year']] = [
|
||||
'total' => 0.00,
|
||||
'http' => 0.00,
|
||||
'ftp' => 0.00,
|
||||
'mail' => 0.00,
|
||||
];
|
||||
}
|
||||
if (empty($days[$item['day'] . '.' . $item['month'] . '.' . $item['year']])) {
|
||||
$days[$item['day'] . '.' . $item['month'] . '.' . $item['year']] = [
|
||||
'total' => 0.00,
|
||||
'http' => 0.00,
|
||||
'ftp' => 0.00,
|
||||
'mail' => 0.00,
|
||||
];
|
||||
}
|
||||
// per year
|
||||
$years[$item['year']]['total'] += $total;
|
||||
$years[$item['year']]['http'] += $http;
|
||||
@@ -86,7 +119,12 @@ class Traffic
|
||||
}
|
||||
|
||||
// calculate overview for given range from users
|
||||
$metrics = [];
|
||||
$metrics = [
|
||||
'total' => 0.00,
|
||||
'http' => 0.00,
|
||||
'ftp' => 0.00,
|
||||
'mail' => 0.00,
|
||||
];
|
||||
foreach ($users as $user) {
|
||||
$metrics['total'] += $user['total'];
|
||||
$metrics['http'] += $user['http'];
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
namespace Froxlor\UI\Callbacks;
|
||||
|
||||
use Froxlor\CurrentUser;
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\Domain\Domain as DDomain;
|
||||
use Froxlor\FileDir;
|
||||
@@ -33,23 +34,36 @@ use Froxlor\UI\Panel\UI;
|
||||
|
||||
class Domain
|
||||
{
|
||||
public static function domainLink(array $attributes)
|
||||
public static function domainEditLink(array $attributes): array
|
||||
{
|
||||
return '<a href="https://' . $attributes['data'] . '" target="_blank">' . $attributes['data'] . '</a>';
|
||||
$linker = UI::getLinker();
|
||||
return [
|
||||
'macro' => 'link',
|
||||
'data' => [
|
||||
'text' => $attributes['data'],
|
||||
'href' => $linker->getLink([
|
||||
'section' => 'domains',
|
||||
'page' => 'domains',
|
||||
'action' => 'edit',
|
||||
'id' => $attributes['fields']['id'],
|
||||
]),
|
||||
'target' => '_blank'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function domainWithCustomerLink(array $attributes)
|
||||
public static function domainWithCustomerLink(array $attributes): string
|
||||
{
|
||||
$linker = UI::getLinker();
|
||||
$result = '<a href="https://' . $attributes['data'] . '" target="_blank">' . $attributes['data'] . '</a>';
|
||||
if ((int)UI::getCurrentUser()['adminsession'] == 1 && $attributes['fields']['customerid']) {
|
||||
$result .= ' (<a href="' . $linker->getLink([
|
||||
'section' => 'customers',
|
||||
'page' => 'customers',
|
||||
'action' => 'su',
|
||||
'sort' => $attributes['fields']['loginname'],
|
||||
'id' => $attributes['fields']['customerid'],
|
||||
]) . '">' . $attributes['fields']['loginname'] . '</a>)';
|
||||
'section' => 'customers',
|
||||
'page' => 'customers',
|
||||
'action' => 'su',
|
||||
'sort' => $attributes['fields']['loginname'],
|
||||
'id' => $attributes['fields']['customerid'],
|
||||
]) . '">' . $attributes['fields']['loginname'] . '</a>)';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@@ -108,12 +122,12 @@ class Domain
|
||||
|
||||
public static function canEdit(array $attributes): bool
|
||||
{
|
||||
return (bool)($attributes['fields']['caneditdomain'] && !$attributes['fields']['deactivated']);
|
||||
return $attributes['fields']['caneditdomain'] && !$attributes['fields']['deactivated'];
|
||||
}
|
||||
|
||||
public static function canViewLogs(array $attributes): bool
|
||||
{
|
||||
if ((int)$attributes['fields']['email_only'] == 0 && !$attributes['fields']['deactivated']) {
|
||||
if ((!CurrentUser::isAdmin() || (CurrentUser::isAdmin() && (int)$attributes['fields']['email_only'] == 0)) && !$attributes['fields']['deactivated']) {
|
||||
if ((int)UI::getCurrentUser()['adminsession'] == 0 && (bool)UI::getCurrentUser()['logviewenabled']) {
|
||||
return true;
|
||||
} elseif ((int)UI::getCurrentUser()['adminsession'] == 1) {
|
||||
@@ -155,17 +169,19 @@ class Domain
|
||||
|
||||
public static function hasLetsEncryptActivated(array $attributes): bool
|
||||
{
|
||||
return ((bool)$attributes['fields']['letsencrypt'] && (int)$attributes['fields']['email_only'] == 0);
|
||||
return ((bool)$attributes['fields']['letsencrypt'] && (!CurrentUser::isAdmin() || (CurrentUser::isAdmin() && (int)$attributes['fields']['email_only'] == 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function canEditSSL(array $attributes): bool
|
||||
{
|
||||
if (
|
||||
Settings::Get('system.use_ssl') == '1'
|
||||
if (Settings::Get('system.use_ssl') == '1'
|
||||
&& DDomain::domainHasSslIpPort($attributes['fields']['id'])
|
||||
&& (int)$attributes['fields']['caneditdomain'] == 1
|
||||
&& (int)$attributes['fields']['letsencrypt'] == 0
|
||||
&& (int)$attributes['fields']['email_only'] == 0
|
||||
&& (!CurrentUser::isAdmin() || (CurrentUser::isAdmin() && (int)$attributes['fields']['email_only'] == 0))
|
||||
&& !$attributes['fields']['deactivated']
|
||||
) {
|
||||
return true;
|
||||
@@ -196,15 +212,15 @@ class Domain
|
||||
],
|
||||
];
|
||||
|
||||
// specified certificate for domain
|
||||
if ($attributes['fields']['domain_hascert'] == 1) {
|
||||
// specified certificate for domain
|
||||
$result['icon'] .= ' text-success';
|
||||
} // shared certificates (e.g. subdomain of domain where certificate is specified)
|
||||
elseif ($attributes['fields']['domain_hascert'] == 2) {
|
||||
} elseif ($attributes['fields']['domain_hascert'] == 2) {
|
||||
// shared certificates (e.g. subdomain of domain where certificate is specified)
|
||||
$result['icon'] .= ' text-warning';
|
||||
$result['title'] .= "\n" . lng('panel.ssleditor_infoshared');
|
||||
} // no certificate specified, using global fallbacks (IPs and Ports or if empty SSL settings)
|
||||
elseif ($attributes['fields']['domain_hascert'] == 0) {
|
||||
} elseif ($attributes['fields']['domain_hascert'] == 0) {
|
||||
// no certificate specified, using global fallbacks (IPs and Ports or if empty SSL settings)
|
||||
$result['icon'] .= ' text-danger';
|
||||
$result['title'] .= "\n" . lng('panel.ssleditor_infoglobal');
|
||||
}
|
||||
@@ -216,7 +232,7 @@ class Domain
|
||||
|
||||
public static function listIPs(array $attributes): string
|
||||
{
|
||||
if (isset($attributes['fields']['ipsandports']) && !empty($attributes['fields']['ipsandports'])) {
|
||||
if (!empty($attributes['fields']['ipsandports'])) {
|
||||
$iplist = "";
|
||||
foreach ($attributes['fields']['ipsandports'] as $ipport) {
|
||||
$iplist .= $ipport['ip'] . ':' . $ipport['port'] . '<br>';
|
||||
@@ -226,6 +242,9 @@ class Domain
|
||||
return lng('panel.empty');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getPhpConfigName(array $attributes): string
|
||||
{
|
||||
$sel_stmt = Database::prepare("SELECT `description` FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id");
|
||||
@@ -233,11 +252,11 @@ class Domain
|
||||
if ((int)UI::getCurrentUser()['adminsession'] == 1) {
|
||||
$linker = UI::getLinker();
|
||||
$result = '<a href="' . $linker->getLink([
|
||||
'section' => 'phpsettings',
|
||||
'page' => 'overview',
|
||||
'searchfield' => 'c.id',
|
||||
'searchtext' => $attributes['data'],
|
||||
]) . '">' . $phpconfig['description'] . '</a>';
|
||||
'section' => 'phpsettings',
|
||||
'page' => 'overview',
|
||||
'searchfield' => 'c.id',
|
||||
'searchtext' => $attributes['data'],
|
||||
]) . '">' . $phpconfig['description'] . '</a>';
|
||||
} else {
|
||||
$result = $phpconfig['description'];
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
namespace Froxlor\UI\Callbacks;
|
||||
|
||||
use Froxlor\CurrentUser;
|
||||
use Froxlor\Settings;
|
||||
|
||||
class Style
|
||||
@@ -68,7 +69,7 @@ class Style
|
||||
$termination_css = 'table-danger';
|
||||
}
|
||||
}
|
||||
$deactivated = $attributes['fields']['deactivated'] || $attributes['fields']['customer_deactivated'];
|
||||
$deactivated = $attributes['fields']['deactivated'] || (CurrentUser::isAdmin() && $attributes['fields']['customer_deactivated']);
|
||||
return $deactivated ? 'table-info' : $termination_css;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,9 +90,10 @@ class Text
|
||||
public static function customerNoteDetailModal(array $attributes): array
|
||||
{
|
||||
$note = $attributes['fields']['custom_notes'] ?? '';
|
||||
$key = $attributes['fields']['customerid'] ?? $attributes['fields']['adminid'];
|
||||
return [
|
||||
'entry' => $attributes['fields']['id'],
|
||||
'id' => 'cnModal' . $attributes['fields']['id'],
|
||||
'entry' => $key,
|
||||
'id' => 'cnModal' . $key,
|
||||
'title' => lng('usersettings.custom_notes.title') . ': ' . ($attributes['fields']['loginname'] ?? $attributes['fields']['adminname']),
|
||||
'body' => nl2br(Markdown::cleanCustomNotes($note))
|
||||
];
|
||||
|
||||
@@ -217,7 +217,8 @@ class Form
|
||||
{
|
||||
$returnvalue = [];
|
||||
if (is_array($fielddata) && isset($fielddata['type']) && $fielddata['type'] == 'select') {
|
||||
if ((!is_array($fielddata['select_var']) || empty($fielddata['select_var'])) && (isset($fielddata['option_options_method']))) {
|
||||
if ((empty($fielddata['select_var']) || !is_array($fielddata['select_var'])) && (isset($fielddata['option_options_method']))
|
||||
) {
|
||||
$returnvalue['select_var'] = call_user_func($fielddata['option_options_method']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ class Response
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function dynamicError($message)
|
||||
public static function dynamicError($message, bool $nosession = false)
|
||||
{
|
||||
$_SESSION['requestData'] = $_POST;
|
||||
$link_ref = '';
|
||||
@@ -147,7 +147,8 @@ class Response
|
||||
$link_ref = htmlentities($_SERVER['HTTP_REFERER']);
|
||||
}
|
||||
|
||||
UI::view('misc/alert.html.twig', [
|
||||
$tpl = $nosession ? 'misc/alert_nosession.html.twig' : 'misc/alert.html.twig';
|
||||
UI::view($tpl, [
|
||||
'type' => 'danger',
|
||||
'btntype' => 'light',
|
||||
'heading' => lng('error.error'),
|
||||
|
||||
@@ -23,4 +23,12 @@ return [
|
||||
* Default: false
|
||||
*/
|
||||
'disable_otp_security_check' => false,
|
||||
|
||||
/**
|
||||
* For debugging/development purposes only.
|
||||
* Enable to display all php related issue (notices, warnings, etc.; depending on php.ini) for froxlor itself
|
||||
*
|
||||
* Default: false
|
||||
*/
|
||||
'display_php_errors' => false,
|
||||
];
|
||||
|
||||
@@ -1657,7 +1657,7 @@ data_directory = /var/lib/postfix
|
||||
# for the case of a subdomain, $mydomain *must* be equal to $myhostname,
|
||||
# otherwise you cannot use the main domain for virtual transport.
|
||||
# also check the note about $mydomain below.
|
||||
myhostname = mail.$mydomain
|
||||
myhostname = $mydomain
|
||||
#myhostname = virtual.domain.tld
|
||||
|
||||
# The mydomain parameter specifies the local internet domain name.
|
||||
@@ -1751,8 +1751,8 @@ inet_interfaces = all
|
||||
#
|
||||
# See also below, section "REJECTING MAIL FOR UNKNOWN LOCAL USERS".
|
||||
#
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain,
|
||||
# mail.$mydomain, www.$mydomain, ftp.$mydomain
|
||||
|
||||
@@ -2561,6 +2561,107 @@ plugin {
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- Antispam services -->
|
||||
<service type="antispam" title="Antispam">
|
||||
<!-- general RSpamd commands -->
|
||||
<general>
|
||||
<commands index="1">
|
||||
<command><![CDATA[mkdir -p /etc/apt/keyrings]]></command>
|
||||
<command><![CDATA[wget -O- https://rspamd.com/apt-stable/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/rspamd.gpg > /dev/null]]></command>
|
||||
<command><![CDATA[echo "deb [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ bookworm main" > /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[echo "deb-src [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ bookworm main" >> /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[apt-get update]]></command>
|
||||
</commands>
|
||||
<installs index="1">
|
||||
<install><![CDATA[DEBIAN_FRONTEND=noninteractive apt-get -yq --no-install-recommends install rspamd]]></install>
|
||||
</installs>
|
||||
<commands index="2">
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/local.d/]]></command>
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/override.d/]]></command>
|
||||
<command><![CDATA[mkdir -p mkdir /var/lib/rspamd/dkim/]]></command>
|
||||
</commands>
|
||||
<files index="1">
|
||||
<file name="/etc/rspamd/local.d/actions.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
# Set rewrite subject to this value (%s is replaced by the original subject)
|
||||
subject = "***SPAM*** %s"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/arc.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
try_fallback = true;
|
||||
### Enable DKIM signing for alias sender addresses
|
||||
allow_username_mismatch = true;
|
||||
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
selector_map = "/etc/rspamd/dkim_selectors.map";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/milter_headers.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
use = ["x-spamd-bar", "x-spam-level", "authentication-results"];
|
||||
authenticated_headers = ["authentication-results"];
|
||||
extended_spam_headers = true
|
||||
skip_local = false
|
||||
skip_authenticated = false
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/replies.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
## If a user has replied to an email, don’t mark other emails in the same thread as spam
|
||||
action = "no action";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/settings.conf"
|
||||
chown="root:root" chmod="0644" backup="true">
|
||||
<content><![CDATA[
|
||||
## Feel free to include your own settings or adjustments here, for example:
|
||||
#whitelist {
|
||||
# priority = low;
|
||||
# rcpt = "postmaster@example.com";
|
||||
# want_spam = yes;
|
||||
#}
|
||||
|
||||
## Include froxlor generated settings
|
||||
.include(try=true,priority=1,duplicate=merge) "{{settings.antispam.config_file}}"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
</files>
|
||||
<commands index="3">
|
||||
<command><![CDATA[cp /etc/rspamd/local.d/arc.conf /etc/rspamd/local.d/dkim_signing.conf]]></command>
|
||||
<command><![CDATA[postconf -e "milter_protocol = 6"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_default_action = accept"]]></command>
|
||||
<command><![CDATA[postconf -e "smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[chown -R _rspamd:_rspamd /var/lib/rspamd/dkim]]></command>
|
||||
<command><![CDATA[chmod 440 /var/lib/rspamd/dkim/*]]></command>
|
||||
<command><![CDATA[service rspamd restart]]></command>
|
||||
<command><![CDATA[service postfix restart]]></command>
|
||||
</commands>
|
||||
</general>
|
||||
<!-- rspamd -->
|
||||
<daemon name="rspamd" title="Rspamd" default="true">
|
||||
<include>//service[@type='antispam']/general/commands[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/installs[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=2]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/files[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=3]
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- FTP services -->
|
||||
<service type="ftp" title="{{lng.admin.configfiles.ftp}}">
|
||||
<!-- Proftpd -->
|
||||
|
||||
@@ -1657,7 +1657,7 @@ data_directory = /var/lib/postfix
|
||||
# for the case of a subdomain, $mydomain *must* be equal to $myhostname,
|
||||
# otherwise you cannot use the main domain for virtual transport.
|
||||
# also check the note about $mydomain below.
|
||||
myhostname = mail.$mydomain
|
||||
myhostname = $mydomain
|
||||
#myhostname = virtual.domain.tld
|
||||
|
||||
# The mydomain parameter specifies the local internet domain name.
|
||||
@@ -1751,8 +1751,8 @@ inet_interfaces = all
|
||||
#
|
||||
# See also below, section "REJECTING MAIL FOR UNKNOWN LOCAL USERS".
|
||||
#
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain,
|
||||
# mail.$mydomain, www.$mydomain, ftp.$mydomain
|
||||
|
||||
@@ -4131,6 +4131,106 @@ plugin {
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- Antispam services -->
|
||||
<service type="antispam" title="Antispam">
|
||||
<!-- general RSpamd commands -->
|
||||
<general>
|
||||
<commands index="1">
|
||||
<command><![CDATA[mkdir -p /etc/apt/keyrings]]></command>
|
||||
<command><![CDATA[wget -O- https://rspamd.com/apt-stable/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/rspamd.gpg > /dev/null]]></command>
|
||||
<command><![CDATA[echo "deb [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ bullseye main" > /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[echo "deb-src [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ bullseye main" >> /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[apt-get update]]></command>
|
||||
</commands>
|
||||
<installs index="1">
|
||||
<install><![CDATA[DEBIAN_FRONTEND=noninteractive apt-get -yq --no-install-recommends install rspamd]]></install>
|
||||
</installs>
|
||||
<commands index="2">
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/local.d/]]></command>
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/override.d/]]></command>
|
||||
<command><![CDATA[mkdir -p mkdir /var/lib/rspamd/dkim/]]></command>
|
||||
</commands>
|
||||
<files index="1">
|
||||
<file name="/etc/rspamd/local.d/actions.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
# Set rewrite subject to this value (%s is replaced by the original subject)
|
||||
subject = "***SPAM*** %s"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/arc.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
try_fallback = true;
|
||||
### Enable DKIM signing for alias sender addresses
|
||||
allow_username_mismatch = true;
|
||||
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
selector_map = "/etc/rspamd/dkim_selectors.map";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/milter_headers.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
use = ["x-spamd-bar", "x-spam-level", "authentication-results"];
|
||||
authenticated_headers = ["authentication-results"];
|
||||
extended_spam_headers = true
|
||||
skip_local = false
|
||||
skip_authenticated = false
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/replies.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
## If a user has replied to an email, don’t mark other emails in the same thread as spam
|
||||
action = "no action";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/settings.conf"
|
||||
chown="root:root" chmod="0644" backup="true">
|
||||
<content><![CDATA[
|
||||
## Feel free to include your own settings or adjustments here, for example:
|
||||
#whitelist {
|
||||
# priority = low;
|
||||
# rcpt = "postmaster@example.com";
|
||||
# want_spam = yes;
|
||||
#}
|
||||
|
||||
## Include froxlor generated settings
|
||||
.include(try=true,priority=1,duplicate=merge) "{{settings.antispam.config_file}}"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
</files>
|
||||
<commands index="3">
|
||||
<command><![CDATA[cp /etc/rspamd/local.d/arc.conf /etc/rspamd/local.d/dkim_signing.conf]]></command>
|
||||
<command><![CDATA[postconf -e "milter_protocol = 6"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_default_action = accept"]]></command>
|
||||
<command><![CDATA[postconf -e "smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[chown -R _rspamd:_rspamd /var/lib/rspamd/dkim]]></command>
|
||||
<command><![CDATA[chmod 440 /var/lib/rspamd/dkim/*]]></command>
|
||||
<command><![CDATA[service rspamd restart]]></command>
|
||||
</commands>
|
||||
</general>
|
||||
<!-- rspamd -->
|
||||
<daemon name="rspamd" title="Rspamd" default="true">
|
||||
<include>//service[@type='antispam']/general/commands[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/installs[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=2]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/files[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=3]
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- FTP services -->
|
||||
<service type="ftp" title="{{lng.admin.configfiles.ftp}}">
|
||||
<!-- Proftpd -->
|
||||
|
||||
@@ -1642,7 +1642,7 @@ compatibility_level = 2
|
||||
# for the case of a subdomain, $mydomain *must* be equal to $myhostname,
|
||||
# otherwise you cannot use the main domain for virtual transport.
|
||||
# also check the note about $mydomain below.
|
||||
myhostname = mail.$mydomain
|
||||
myhostname = $mydomain
|
||||
#myhostname = virtual.domain.tld
|
||||
|
||||
# The mydomain parameter specifies the local internet domain name.
|
||||
@@ -1656,8 +1656,8 @@ myhostname = mail.$mydomain
|
||||
# FQDN from Froxlor
|
||||
mydomain = <SERVERNAME>
|
||||
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain,
|
||||
# mail.$mydomain, www.$mydomain, ftp.$mydomain
|
||||
|
||||
@@ -3354,6 +3354,106 @@ plugin {
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- Antispam services -->
|
||||
<service type="antispam" title="Antispam">
|
||||
<!-- general RSpamd commands -->
|
||||
<general>
|
||||
<commands index="1">
|
||||
<command><![CDATA[mkdir -p /etc/apt/keyrings]]></command>
|
||||
<command><![CDATA[wget -O- https://rspamd.com/apt-stable/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/rspamd.gpg > /dev/null]]></command>
|
||||
<command><![CDATA[echo "deb [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ focal main" > /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[echo "deb-src [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ focal main" >> /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[apt-get update]]></command>
|
||||
</commands>
|
||||
<installs index="1">
|
||||
<install><![CDATA[DEBIAN_FRONTEND=noninteractive apt-get -yq --no-install-recommends install rspamd]]></install>
|
||||
</installs>
|
||||
<commands index="2">
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/local.d/]]></command>
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/override.d/]]></command>
|
||||
<command><![CDATA[mkdir -p mkdir /var/lib/rspamd/dkim/]]></command>
|
||||
</commands>
|
||||
<files index="1">
|
||||
<file name="/etc/rspamd/local.d/actions.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
# Set rewrite subject to this value (%s is replaced by the original subject)
|
||||
subject = "***SPAM*** %s"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/arc.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
try_fallback = true;
|
||||
### Enable DKIM signing for alias sender addresses
|
||||
allow_username_mismatch = true;
|
||||
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
selector_map = "/etc/rspamd/dkim_selectors.map";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/milter_headers.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
use = ["x-spamd-bar", "x-spam-level", "authentication-results"];
|
||||
authenticated_headers = ["authentication-results"];
|
||||
extended_spam_headers = true
|
||||
skip_local = false
|
||||
skip_authenticated = false
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/replies.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
## If a user has replied to an email, don’t mark other emails in the same thread as spam
|
||||
action = "no action";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/settings.conf"
|
||||
chown="root:root" chmod="0644" backup="true">
|
||||
<content><![CDATA[
|
||||
## Feel free to include your own settings or adjustments here, for example:
|
||||
#whitelist {
|
||||
# priority = low;
|
||||
# rcpt = "postmaster@example.com";
|
||||
# want_spam = yes;
|
||||
#}
|
||||
|
||||
## Include froxlor generated settings
|
||||
.include(try=true,priority=1,duplicate=merge) "{{settings.antispam.config_file}}"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
</files>
|
||||
<commands index="3">
|
||||
<command><![CDATA[cp /etc/rspamd/local.d/arc.conf /etc/rspamd/local.d/dkim_signing.conf]]></command>
|
||||
<command><![CDATA[postconf -e "milter_protocol = 6"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_default_action = accept"]]></command>
|
||||
<command><![CDATA[postconf -e "smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[chown -R _rspamd:_rspamd /var/lib/rspamd/dkim]]></command>
|
||||
<command><![CDATA[chmod 440 /var/lib/rspamd/dkim/*]]></command>
|
||||
<command><![CDATA[service rspamd restart]]></command>
|
||||
</commands>
|
||||
</general>
|
||||
<!-- rspamd -->
|
||||
<daemon name="rspamd" title="Rspamd" default="true">
|
||||
<include>//service[@type='antispam']/general/commands[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/installs[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=2]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/files[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=3]
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- FTP services -->
|
||||
<service type="ftp" title="{{lng.admin.configfiles.ftp}}">
|
||||
<!-- Proftpd -->
|
||||
|
||||
@@ -1727,12 +1727,9 @@ compatibility_level = 2
|
||||
## General Postfix configuration
|
||||
# should be the default domain from your provider eg. "server100.provider.tld"
|
||||
mydomain = <SERVERNAME>
|
||||
|
||||
# should be different from $mydomain eg. "mail.$mydomain"
|
||||
myhostname = mail.$mydomain
|
||||
myhostname = $mydomain
|
||||
|
||||
mydestination = $myhostname,
|
||||
$mydomain,
|
||||
localhost.$myhostname,
|
||||
localhost.$mydomain,
|
||||
localhost
|
||||
@@ -2218,6 +2215,98 @@ plugin {
|
||||
<command><![CDATA[/etc/init.d/dovecot restart]]></command>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- Antispam services -->
|
||||
<service type="antispam" title="Antispam">
|
||||
<!-- general RSpamd commands -->
|
||||
<general>
|
||||
<installs index="1">
|
||||
<install><![CDATA[emerge mail-filter/rspamd]]></install>
|
||||
</installs>
|
||||
<commands index="2">
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/local.d/]]></command>
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/override.d/]]></command>
|
||||
<command><![CDATA[mkdir -p mkdir /var/lib/rspamd/dkim/]]></command>
|
||||
</commands>
|
||||
<files index="1">
|
||||
<file name="/etc/rspamd/local.d/actions.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
# Set rewrite subject to this value (%s is replaced by the original subject)
|
||||
subject = "***SPAM*** %s"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/arc.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
try_fallback = true;
|
||||
### Enable DKIM signing for alias sender addresses
|
||||
allow_username_mismatch = true;
|
||||
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
selector_map = "/etc/rspamd/dkim_selectors.map";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/milter_headers.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
use = ["x-spamd-bar", "x-spam-level", "authentication-results"];
|
||||
authenticated_headers = ["authentication-results"];
|
||||
extended_spam_headers = true
|
||||
skip_local = false
|
||||
skip_authenticated = false
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/replies.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
## If a user has replied to an email, don’t mark other emails in the same thread as spam
|
||||
action = "no action";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/settings.conf"
|
||||
chown="root:root" chmod="0644" backup="true">
|
||||
<content><![CDATA[
|
||||
## Feel free to include your own settings or adjustments here, for example:
|
||||
#whitelist {
|
||||
# priority = low;
|
||||
# rcpt = "postmaster@example.com";
|
||||
# want_spam = yes;
|
||||
#}
|
||||
|
||||
## Include froxlor generated settings
|
||||
.include(try=true,priority=1,duplicate=merge) "{{settings.antispam.config_file}}"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
</files>
|
||||
<commands index="3">
|
||||
<command><![CDATA[cp /etc/rspamd/local.d/arc.conf /etc/rspamd/local.d/dkim_signing.conf]]></command>
|
||||
<command><![CDATA[postconf -e "milter_protocol = 6"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_default_action = accept"]]></command>
|
||||
<command><![CDATA[postconf -e "smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[chown -R _rspamd:_rspamd /var/lib/rspamd/dkim]]></command>
|
||||
<command><![CDATA[chmod 440 /var/lib/rspamd/dkim/*]]></command>
|
||||
<command><![CDATA[rc-update add rspamd default]]></command>
|
||||
<command><![CDATA[/etc/init.d/rspamd restart]]></command>
|
||||
</commands>
|
||||
</general>
|
||||
<!-- rspamd -->
|
||||
<daemon name="rspamd" title="Rspamd" default="true">
|
||||
<include>//service[@type='antispam']/general/installs[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=2]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/files[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=3]
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- FTP services -->
|
||||
<service type="ftp" title="{{lng.admin.configfiles.ftp}}">
|
||||
<!-- Proftpd -->
|
||||
|
||||
@@ -1642,7 +1642,7 @@ compatibility_level = 2
|
||||
# for the case of a subdomain, $mydomain *must* be equal to $myhostname,
|
||||
# otherwise you cannot use the main domain for virtual transport.
|
||||
# also check the note about $mydomain below.
|
||||
myhostname = mail.$mydomain
|
||||
myhostname = $mydomain
|
||||
#myhostname = virtual.domain.tld
|
||||
|
||||
# The mydomain parameter specifies the local internet domain name.
|
||||
@@ -1656,8 +1656,8 @@ myhostname = mail.$mydomain
|
||||
# FQDN from Froxlor
|
||||
mydomain = <SERVERNAME>
|
||||
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
mydestination = $myhostname, localhost.$mydomain, localhost
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
|
||||
#mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain,
|
||||
# mail.$mydomain, www.$mydomain, ftp.$mydomain
|
||||
|
||||
@@ -3344,6 +3344,106 @@ plugin {
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- Antispam services -->
|
||||
<service type="antispam" title="Antispam">
|
||||
<!-- general RSpamd commands -->
|
||||
<general>
|
||||
<commands index="1">
|
||||
<command><![CDATA[mkdir -p /etc/apt/keyrings]]></command>
|
||||
<command><![CDATA[wget -O- https://rspamd.com/apt-stable/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/rspamd.gpg > /dev/null]]></command>
|
||||
<command><![CDATA[echo "deb [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ jammy main" > /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[echo "deb-src [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ jammy main" >> /etc/apt/sources.list.d/rspamd.list]]></command>
|
||||
<command><![CDATA[apt-get update]]></command>
|
||||
</commands>
|
||||
<installs index="1">
|
||||
<install><![CDATA[DEBIAN_FRONTEND=noninteractive apt-get -yq --no-install-recommends install rspamd]]></install>
|
||||
</installs>
|
||||
<commands index="2">
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/local.d/]]></command>
|
||||
<command><![CDATA[mkdir -p /etc/rspamd/override.d/]]></command>
|
||||
<command><![CDATA[mkdir -p mkdir /var/lib/rspamd/dkim/]]></command>
|
||||
</commands>
|
||||
<files index="1">
|
||||
<file name="/etc/rspamd/local.d/actions.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
# Set rewrite subject to this value (%s is replaced by the original subject)
|
||||
subject = "***SPAM*** %s"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/arc.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
try_fallback = true;
|
||||
### Enable DKIM signing for alias sender addresses
|
||||
allow_username_mismatch = true;
|
||||
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
selector_map = "/etc/rspamd/dkim_selectors.map";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/milter_headers.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
use = ["x-spamd-bar", "x-spam-level", "authentication-results"];
|
||||
authenticated_headers = ["authentication-results"];
|
||||
extended_spam_headers = true
|
||||
skip_local = false
|
||||
skip_authenticated = false
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/replies.conf"
|
||||
chown="root:root" chmod="0644">
|
||||
<content><![CDATA[
|
||||
## If a user has replied to an email, don’t mark other emails in the same thread as spam
|
||||
action = "no action";
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
<file name="/etc/rspamd/local.d/settings.conf"
|
||||
chown="root:root" chmod="0644" backup="true">
|
||||
<content><![CDATA[
|
||||
## Feel free to include your own settings or adjustments here, for example:
|
||||
#whitelist {
|
||||
# priority = low;
|
||||
# rcpt = "postmaster@example.com";
|
||||
# want_spam = yes;
|
||||
#}
|
||||
|
||||
## Include froxlor generated settings
|
||||
.include(try=true,priority=1,duplicate=merge) "{{settings.antispam.config_file}}"
|
||||
]]>
|
||||
</content>
|
||||
</file>
|
||||
</files>
|
||||
<commands index="3">
|
||||
<command><![CDATA[cp /etc/rspamd/local.d/arc.conf /etc/rspamd/local.d/dkim_signing.conf]]></command>
|
||||
<command><![CDATA[postconf -e "milter_protocol = 6"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"]]></command>
|
||||
<command><![CDATA[postconf -e "milter_default_action = accept"]]></command>
|
||||
<command><![CDATA[postconf -e "smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"]]></command>
|
||||
<command><![CDATA[chown -R _rspamd:_rspamd /var/lib/rspamd/dkim]]></command>
|
||||
<command><![CDATA[chmod 440 /var/lib/rspamd/dkim/*]]></command>
|
||||
<command><![CDATA[service rspamd restart]]></command>
|
||||
</commands>
|
||||
</general>
|
||||
<!-- rspamd -->
|
||||
<daemon name="rspamd" title="Rspamd" default="true">
|
||||
<include>//service[@type='antispam']/general/commands[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/installs[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=2]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/files[@index=1]
|
||||
</include>
|
||||
<include>//service[@type='antispam']/general/commands[@index=3]
|
||||
</include>
|
||||
</daemon>
|
||||
</service>
|
||||
<!-- FTP services -->
|
||||
<service type="ftp" title="{{lng.admin.configfiles.ftp}}">
|
||||
<!-- Proftpd -->
|
||||
|
||||
@@ -111,7 +111,7 @@ return [
|
||||
'selected' => 0
|
||||
],
|
||||
'dkim' => [
|
||||
'visible' => Settings::Get('dkim.use_dkim') == '1',
|
||||
'visible' => Settings::Get('antispam.activated') == '1',
|
||||
'label' => 'DomainKeys',
|
||||
'type' => 'checkbox',
|
||||
'value' => '1',
|
||||
|
||||
@@ -129,7 +129,7 @@ return [
|
||||
'selected' => $result['subcanemaildomain']
|
||||
],
|
||||
'dkim' => [
|
||||
'visible' => Settings::Get('dkim.use_dkim') == '1',
|
||||
'visible' => Settings::Get('antispam.activated') == '1',
|
||||
'label' => 'DomainKeys',
|
||||
'type' => 'checkbox',
|
||||
'value' => '1',
|
||||
|
||||
@@ -116,6 +116,12 @@ return [
|
||||
'type' => 'hidden',
|
||||
'value' => '0'
|
||||
],
|
||||
'dkim_entry' => [
|
||||
'visible' => (Settings::Get('system.bind_enable') == '0' && Settings::Get('antispam.activated') == '1' && $result['dkim'] == '1' && $result['dkim_pubkey'] != ''),
|
||||
'label' => lng('antispam.required_dkim_dns'),
|
||||
'type' => 'longtext',
|
||||
'value' => (string)(new \Froxlor\Dns\DnsEntry('dkim' . $result['dkim_id'] . '._domainkey', 'TXT', '"v=DKIM1; k=rsa; p='.trim($result['dkim_pubkey']).'"'))
|
||||
],
|
||||
]
|
||||
],
|
||||
'section_bssl' => [
|
||||
|
||||
@@ -102,6 +102,44 @@ return [
|
||||
]
|
||||
]
|
||||
],
|
||||
'spam_tag_level' => [
|
||||
'label' => lng('antispam.spam_tag_level'),
|
||||
'type' => 'text',
|
||||
'string_regexp' => '/^\d{1,}(\.\d{1,2})?$/',
|
||||
'value' => $result['spam_tag_level']
|
||||
],
|
||||
'spam_kill_level' => [
|
||||
'label' => lng('antispam.spam_kill_level'),
|
||||
'type' => 'text',
|
||||
'string_regexp' => '/^\d{1,}(\.\d{1,2})?$/',
|
||||
'value' => $result['spam_kill_level']
|
||||
],
|
||||
'bypass_spam' => [
|
||||
'label' => lng('antispam.bypass_spam'),
|
||||
'type' => 'label',
|
||||
'value' => ((int)$result['bypass_spam'] == 0 ? lng('panel.no') : lng('panel.yes')),
|
||||
'next_to' => [
|
||||
'add_link' => [
|
||||
'type' => 'link',
|
||||
'href' => $filename . '?page=' . $page . '&domainid=' . $result['domainid'] . '&action=togglebypass&id=' . $result['id'],
|
||||
'label' => '<i class="fa-solid fa-arrow-right-arrow-left"></i> ' . lng('panel.toggle'),
|
||||
'classes' => 'btn btn-sm btn-secondary'
|
||||
]
|
||||
]
|
||||
],
|
||||
'policy_greylist' => [
|
||||
'label' => lng('antispam.policy_greylist'),
|
||||
'type' => 'label',
|
||||
'value' => ((int)$result['policy_greylist'] == 0 ? lng('panel.no') : lng('panel.yes')),
|
||||
'next_to' => [
|
||||
'add_link' => [
|
||||
'type' => 'link',
|
||||
'href' => $filename . '?page=' . $page . '&domainid=' . $result['domainid'] . '&action=togglegreylist&id=' . $result['id'],
|
||||
'label' => '<i class="fa-solid fa-arrow-right-arrow-left"></i> ' . lng('panel.toggle'),
|
||||
'classes' => 'btn btn-sm btn-secondary'
|
||||
]
|
||||
]
|
||||
],
|
||||
'mail_fwds' => [
|
||||
'label' => lng('emails.forwarders') . ' (' . $forwarders_count . ')',
|
||||
'type' => 'itemlist',
|
||||
@@ -119,7 +157,9 @@ return [
|
||||
]
|
||||
],
|
||||
'buttons' => [
|
||||
/* none */
|
||||
[
|
||||
'label' => lng('panel.save')
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
@@ -50,7 +50,7 @@ return [
|
||||
'label' => lng('domains.domainname'),
|
||||
'field' => 'domain_ace',
|
||||
'isdefaultsearchfield' => true,
|
||||
'callback' => [Domain::class, 'domainLink'],
|
||||
'callback' => [Domain::class, 'domainEditLink'],
|
||||
],
|
||||
'ipsandports' => [
|
||||
'label' => lng('admin.ipsandports.ipsandports'),
|
||||
|
||||
@@ -49,6 +49,24 @@ return [
|
||||
'field' => 'popaccountid',
|
||||
'callback' => [Email::class, 'account'],
|
||||
],
|
||||
'm.spam_tag_level' => [
|
||||
'label' => lng('emails.spam_tag_level'),
|
||||
'field' => 'spam_tag_level',
|
||||
],
|
||||
'm.spam_kill_level' => [
|
||||
'label' => lng('emails.spam_kill_level'),
|
||||
'field' => 'spam_kill_level',
|
||||
],
|
||||
'm.bypass_spam' => [
|
||||
'label' => lng('emails.bypass_spam'),
|
||||
'field' => 'bypass_spam',
|
||||
'callback' => [Text::class, 'boolean'],
|
||||
],
|
||||
'm.policy_greylist' => [
|
||||
'label' => lng('emails.policy_greylist'),
|
||||
'field' => 'policy_greylist',
|
||||
'callback' => [Text::class, 'boolean'],
|
||||
],
|
||||
'm.iscatchall' => [
|
||||
'label' => lng('emails.catchall'),
|
||||
'field' => 'iscatchall',
|
||||
|
||||
Reference in New Issue
Block a user