...
 
Commits (3)
......@@ -8,6 +8,8 @@ touch /etc/rspamd/rspamd.conf.local \
chmod 755 /var/lib/rspamd
addgroup --system --gid 82 www-access
[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc
DOVECOT_V4=
......@@ -49,8 +51,10 @@ touch /etc/rspamd/custom/global_mime_from_blacklist.map \
/etc/rspamd/custom/bad_words_de.map
# www-data (82) group needs to write to these files
chown -R _rspamd:82 /etc/rspamd/custom
chmod -R g+w /etc/rspamd/custom
chown root:root /etc/rspamd/custom/
chmod 0755 /etc/rspamd/custom/
chown -R _rspamd:www-access /etc/rspamd/custom/*
chmod -R 664 /etc/rspamd/custom/*
# Run hooks
for file in /hooks/*; do
......
......@@ -17,6 +17,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
<li role="presentation"><a href="#tab-routing" aria-controls="tab-config" role="tab" data-toggle="tab"><?=$lang['admin']['routing'];?></a></li>
<li role="presentation"><a href="#tab-sys-mails" aria-controls="tab-sys-mails" role="tab" data-toggle="tab"><?=$lang['admin']['sys_mails'];?></a></li>
<li role="presentation"><a href="#tab-mailq" aria-controls="tab-mailq" role="tab" data-toggle="tab"><?=$lang['admin']['queue_manager'];?></a></li>
<li role="presentation"><a href="#tab-rspamdmaps" aria-controls="tab-rspamdmaps" role="tab" data-toggle="tab"><?=$lang['admin']['rspamd_global_filters'];?></a></li>
</ul>
<div class="row">
......@@ -1135,6 +1136,49 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tab-rspamdmaps">
<div class="panel panel-default">
<div class="panel-heading">
<?=$lang['admin']['rspamd_global_filters'];?>
</div>
<div class="panel-body">
<p><?=$lang['admin']['rspamd_global_filters_info'];?></p>
<div id="confirm_show_rspamd_global_filters" class="<?=($_SESSION['show_rspamd_global_filters'] === true) ? 'hidden' : '';?>">
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<label>
<input type="checkbox" id="show_rspamd_global_filters"> <?=$lang['admin']['rspamd_global_filters_agree'];?>
</label>
</div>
</div>
</div>
<div id="rspamd_global_filters" class="<?=($_SESSION['show_rspamd_global_filters'] !== true) ? 'hidden' : '';?>">
<?php
foreach ($RSPAMD_MAPS as $rspamd_map):
?>
<hr>
<form class="form-horizontal" data-id="<?=$rspamd_map;?>" role="form" method="post">
<div class="form-group">
<label class="control-label col-sm-3" for="<?=$rspamd_map;?>"><code><?=$rspamd_map;?></code>:</label>
<div class="col-sm-9">
<textarea id="<?=$rspamd_map;?>" spellcheck="false" autocorrect="off" autocapitalize="none" class="form-control textarea-code" rows="10" name="rspamd_map_data" required><?=file_get_contents('/rspamd_custom_maps/' . $rspamd_map);?></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button class="btn btn-xs btn-default validate_rspamd_regex" data-regex-map="<?=$rspamd_map;?>" href="#"><?=$lang['add']['validate'];?></button>
<button class="btn btn-xs btn-success submit_rspamd_regex" data-action="edit_selected" data-id="<?=$rspamd_map;?>" data-item="<?=htmlspecialchars($rspamd_map);?>" data-api-url='edit/rspamd-map' data-api-attr='{}' href="#" disabled><?=$lang['edit']['save'];?></button>
</div>
</div>
</form>
<?php
endforeach;
?>
</div>
</div>
</div>
</div>
</div> <!-- /tab-content -->
</div> <!-- /col-md-12 -->
......
<?php
session_start();
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
header('Content-Type: application/json');
if (!isset($_SESSION['mailcow_cc_role'])) {
exit();
}
if (isset($_GET['regex'])) {
$regex_lines = preg_split("/(\r\n|\n|\r)/", $_GET['regex']);
foreach ($regex_lines as $line => $regex) {
if (empty($regex) || substr($regex, 0, 1) == "#") {
continue;
}
if (empty($regex) || substr($regex, 0, 1) != "/") {
echo json_encode(array('type' => 'danger', 'msg' => 'Line ' . ($line + 1) . ': Invalid regex'));
exit();
}
if (@preg_match($regex, 'Lorem Ipsum') === false) {
echo json_encode(array('type' => 'danger', 'msg' => 'Line ' . ($line + 1) . ': Invalid regex "' . $regex . '"'));
exit();
}
}
echo json_encode(array('type' => 'success', 'msg' => $lang['add']['validation_success']));
}
?>
<?php
session_start();
$_SESSION['show_rspamd_global_filters'] = true;
\ No newline at end of file
......@@ -32,7 +32,7 @@ function fail2ban($_action, $_data = null) {
$tmp_wl_data[] = $key;
}
if (isset($tmp_wl_data)) {
sort($tmp_wl_data);
natsort($tmp_wl_data);
$f2b_options['whitelist'] = implode(PHP_EOL, $tmp_wl_data);
}
else {
......@@ -48,7 +48,7 @@ function fail2ban($_action, $_data = null) {
$tmp_bl_data[] = $key;
}
if (isset($tmp_bl_data)) {
sort($tmp_bl_data);
natsort($tmp_bl_data);
$f2b_options['blacklist'] = implode(PHP_EOL, $tmp_bl_data);
}
else {
......
......@@ -157,4 +157,155 @@ function rsettings($_action, $_data = null) {
return $settingsmapdata;
break;
}
}
function rspamd($_action, $_data = null) {
global $pdo;
global $lang;
global $RSPAMD_MAPS;
$_data_log = $_data;
switch ($_action) {
case 'add':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$content = $_data['content'];
$desc = $_data['desc'];
$active = intval($_data['active']);
if (empty($content)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'map_content_empty'
);
return false;
}
try {
$stmt = $pdo->prepare("INSERT INTO `settingsmap` (`content`, `desc`, `active`)
VALUES (:content, :desc, :active)");
$stmt->execute(array(
':content' => $content,
':desc' => $desc,
':active' => $active
));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('mysql_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'settings_map_added'
);
break;
case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$maps = (array)$_data['map'];
foreach ($maps as $map) {
if (!in_array($map, $RSPAMD_MAPS)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('global_map_invalid', $map)
);
continue;
}
try {
if (file_exists('/rspamd_custom_maps/' . $map)) {
$map_content = trim($_data['rspamd_map_data']);
$map_handle = fopen('/rspamd_custom_maps/' . $map, 'w');
if (!$map_handle) {
throw new Exception('File cannot be opened for writing.');
}
fwrite($map_handle, $map_content . PHP_EOL);
fclose($map_handle);
}
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('global_map_write_error', htmlspecialchars($map), htmlspecialchars($e->getMessage()))
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('object_modified', htmlspecialchars($map))
);
}
break;
case 'delete':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$ids = (array)$_data['id'];
foreach ($ids as $id) {
try {
$stmt = $pdo->prepare("DELETE FROM `settingsmap` WHERE `id`= :id");
$stmt->execute(array(':id' => $id));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('mysql_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('settings_map_removed', htmlspecialchars($id))
);
}
break;
case 'get':
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
$settingsmaps = array();
$stmt = $pdo->query("SELECT `id`, `desc`, `active` FROM `settingsmap`");
$settingsmaps = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $settingsmaps;
break;
case 'details':
if ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
return false;
}
$settingsmapdata = array();
$stmt = $pdo->prepare("SELECT `id`,
`desc`,
`content`,
`active` AS `active_int`,
CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
FROM `settingsmap`
WHERE `id` = :id");
$stmt->execute(array(':id' => $_data));
$settingsmapdata = $stmt->fetch(PDO::FETCH_ASSOC);
return $settingsmapdata;
break;
}
}
\ No newline at end of file
......@@ -209,7 +209,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailq.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.oauth2.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.transports.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rsettings.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rspamd.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
......
......@@ -154,3 +154,13 @@ $MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'] = 'hourly';
// Default mailbox format, should not be changed unless you know exactly, what you do, keep the trailing ":"
// Check dovecot.conf for further changes (e.g. shared namespace)
$MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';
// Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
$RSPAMD_MAPS = array(
'global_mime_from_blacklist.map',
'global_mime_from_whitelist.map',
'global_rcpt_blacklist.map',
'global_rcpt_whitelist.map',
'global_smtp_from_blacklist.map',
'global_smtp_from_whitelist.map'
);
......@@ -3,6 +3,7 @@ var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456
jQuery(function($){
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
function jq(myid) {return "#" + myid.replace( /(:|\.|\[|\]|,|=|@)/g, "\\$1" );}
function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
function hashCode(t){for(var n=0,r=0;r<t.length;r++)n=t.charCodeAt(r)+((n<<5)-n);return n}
......@@ -29,6 +30,33 @@ jQuery(function($){
$("#mass_exclude").change(function(){ $("#mass_include").selectpicker('deselectAll'); });
$("#mass_include").change(function(){ $("#mass_exclude").selectpicker('deselectAll'); });
$("#mass_disarm").click(function() { $("#mass_send").attr("disabled", !this.checked); });
$(".validate_rspamd_regex").click(function( event ) {
event.preventDefault();
var regex_map_id = $(this).data('regex-map');
var regex_data = $(jq(regex_map_id)).val();
$.ajax({
dataType: 'json',
url: "/inc/ajax/regex_validation.php",
type: "get",
data: { regex: regex_data },
complete: function(data) {
var response = (data.responseText);
response_obj = JSON.parse(response);
if (response_obj.type == "success") {
$('button[data-id="' + regex_map_id + '"]').attr({"disabled": false});
}
mailcow_alert_box(response_obj.msg, response_obj.type);
},
});
});
$('.textarea-code').on('keyup', function() {
$('.submit_rspamd_regex').attr({"disabled": true});
});
$("#show_rspamd_global_filters").click(function() {
$.get("inc/ajax/show_rspamd_global_filters.php");
$("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("hidden");
});
$("#super_delete").click(function() { return confirm(lang.queue_ays); });
$(".refresh_table").on('click', function(e) {
e.preventDefault();
......
......@@ -133,16 +133,15 @@ $(document).ready(function() {
$(e.currentTarget).find('#sieveDataText').html('<pre style="font-size:14px;line-height:1.1">' + sieveScript + '</pre>');
});
// Disable submit button on script change
$('#script_data').on('keyup', function() {
$('.textarea-code').on('keyup', function() {
$('#add_filter_btns > #add_sieve_script').attr({"disabled": true});
$('#validation_msg').html('-');
});
// Validate script data
$("#validate_sieve").click(function( event ) {
event.preventDefault();
var script = $('#script_data').val();
$.ajax({
dataType: 'jsonp',
dataType: 'json',
url: "/inc/ajax/sieve_validation.php",
type: "get",
data: { script: script },
......
......@@ -1245,6 +1245,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
case "alias":
process_edit_return(mailbox('edit', 'alias', array_merge(array('id' => $items), $attr)));
break;
case "rspamd-map":
process_edit_return(rspamd('edit', array_merge(array('map' => $items), $attr)));
break;
case "app_links":
process_edit_return(customize('edit', 'app_links', $attr));
break;
......
......@@ -71,6 +71,8 @@ $lang['danger']['private_key_error'] = "Schlüsselfehler: %s";
$lang['danger']['map_content_empty'] = "Inhalt darf nicht leer sein";
$lang['success']['settings_map_added'] = "Regel wurde gespeichert";
$lang['danger']['settings_map_invalid'] = "Regel ID %s ist ungültig";
$lang['danger']['global_map_invalid'] = "Rspamd Map %s ist ungültig";
$lang['danger']['global_map_write_error'] = "Kann globale Map ID %s nicht schreiben: %s";
$lang['success']['settings_map_removed'] = "Regeln wurden entfernt: %s";
$lang['danger']['invalid_host'] = "Ungültiger Host: %s";
$lang['danger']['relayhost_invalid'] = "Mapeintrag %s ist ungültig";
......@@ -915,5 +917,9 @@ $lang['mailbox']['alias_domain_backupmx'] = 'Alias-Domain für Relay-Domain inak
$lang['danger']['extra_acl_invalid'] = 'Externe Absenderadresse "%s" ist ungültig';
$lang['danger']['extra_acl_invalid_domain'] = 'Externe Absenderadresse "%s" verwendet eine ungültige Domain';
$lang['admin']['rspamd_global_filters'] = 'Globale Rspamd Filter';
$lang['admin']['rspamd_global_filters'] = 'Globale Rspamd Filter';
$lang['admin']['rspamd_global_filters_agree'] = "Ich werde vorsichtig sein!";
$lang['admin']['rspamd_global_filters'] = 'Globale Filter-Maps';
$lang['admin']['rspamd_global_filters_info'] = 'Globale Filter-Maps steuern globales White- und Blacklisting dieses Servers. Die akzeptierte Form für Einträge sind <b>ausschließlich</b> Regular Expressions.
Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.<br>
Das korrekte Format lautet "/pattern/options" (Beispiel: <code>/.+@domain\.tld/i</code>).<br>
Der Name der Map beschreibt die jeweilige Funktion.';
......@@ -72,6 +72,8 @@ $lang['danger']['private_key_error'] = "Private key error: %s";
$lang['danger']['map_content_empty'] = "Map content cannot be empty";
$lang['success']['settings_map_added'] = "Added settings map entry";
$lang['danger']['settings_map_invalid'] = "Settings map ID %s invalid";
$lang['danger']['global_map_invalid'] = "Global map ID %s invalid";
$lang['danger']['global_map_write_error'] = "Could not write global map ID %s: %s";
$lang['success']['settings_map_removed'] = "Removed settings map ID %s";
$lang['danger']['invalid_host'] = "Invalid host specified: %s";
$lang['danger']['relayhost_invalid'] = "Map entry %s is invalid";
......@@ -940,5 +942,8 @@ $lang['mailbox']['alias_domain_backupmx'] = 'Alias domain inactive for relay dom
$lang['danger']['extra_acl_invalid'] = 'External sender address "%s" is invalid';
$lang['danger']['extra_acl_invalid_domain'] = 'External sender "%s" uses an invalid domain';
$lang['admin']['rspamd_global_filters'] = 'Global Rspamd filters';
$lang['admin']['rspamd_global_filters_info'] = 'Global Rspamd filters';
$lang['admin']['rspamd_global_filters_agree'] = "I will be careful!";
$lang['admin']['rspamd_global_filters'] = 'Global filter maps';
$lang['admin']['rspamd_global_filters_info'] = 'Global filter maps contain different kind of global black and whitelists. Their names explain their purpose. All content must contain valid regular expression in the format of "/pattern/options" (e.g. <code>/.+@domain\.tld/i</code>).<br>
Although rudimentary checks are being executed on each line of regex, Rspamds functionality can be broken, if it fails to read the syntax correctly.';
......@@ -68,7 +68,7 @@ services:
- clamd
rspamd-mailcow:
image: mailcow/rspamd:1.49
image: mailcow/rspamd:1.50
build: ./data/Dockerfiles/rspamd
stop_grace_period: 30s
depends_on:
......@@ -105,7 +105,7 @@ services:
- ./data/hooks/dovecot:/hooks
- ./data/web:/web:rw
- ./data/conf/rspamd/dynmaps:/dynmaps:ro
- ./data/conf/rspamd/custom/:/rspamd_custom_maps:rw
- ./data/conf/rspamd/custom/:/rspamd_custom_maps
- rspamd-vol-1:/var/lib/rspamd
- mysql-socket-vol-1:/var/run/mysqld/
- ./data/conf/sogo/:/etc/sogo/
......