public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] Autodiscover returns given password decoded and trimed; Add sieve pre and post filters to UI; Move ajax called files; Rework log system: 100 entries per default, add more per click; Syncjobs: Do not read log to data attribute
85d1ee2f
data/web/admin.php | 56 +-
data/web/api.php | 32 -
data/web/autodiscover.php | 3 +-
data/web/css/footable.bootstrap.min.css | 5 +-
data/web/css/mailbox.css | 2 +-
data/web/css/numberedtextarea.min.css | 1 +
data/web/css/user.css | 7 +
data/web/edit.php | 85 ++-
data/web/inc/ajax/sieve_validation.php | 23 +
data/web/inc/ajax/sogo_ctrl.php | 39 ++
data/web/inc/ajax/syncjob_logs.php | 15 +
data/web/inc/footer.inc.php | 16 +-
...call_sogo_ctrl.php => functions.docker.inc.php} | 39 +-
data/web/inc/functions.inc.php | 63 +-
data/web/inc/functions.mailbox.inc.php | 376 +++++++++-
data/web/inc/header.inc.php | 1 +
data/web/inc/init_db.inc.php | 46 +-
data/web/inc/lib/sieve/SieveDumpable.php | 7 +
data/web/inc/lib/sieve/SieveException.php | 47 ++
data/web/inc/lib/sieve/SieveKeywordRegistry.php | 233 ++++++
data/web/inc/lib/sieve/SieveParser.php | 255 +++++++
data/web/inc/lib/sieve/SieveScanner.php | 145 ++++
data/web/inc/lib/sieve/SieveScript.php | 6 +
data/web/inc/lib/sieve/SieveSemantics.php | 611 ++++++++++++++++
data/web/inc/lib/sieve/SieveToken.php | 88 +++
data/web/inc/lib/sieve/SieveTree.php | 117 ++++
data/web/inc/lib/sieve/extensions/body.xml | 14 +
.../sieve/extensions/comparator-ascii-numeric.xml | 7 +
data/web/inc/lib/sieve/extensions/copy.xml | 9 +
data/web/inc/lib/sieve/extensions/date.xml | 28 +
data/web/inc/lib/sieve/extensions/editheader.xml | 22 +
data/web/inc/lib/sieve/extensions/envelope.xml | 13 +
data/web/inc/lib/sieve/extensions/environment.xml | 13 +
data/web/inc/lib/sieve/extensions/ereject.xml | 11 +
data/web/inc/lib/sieve/extensions/fileinto.xml | 9 +
data/web/inc/lib/sieve/extensions/imap4flags.xml | 29 +
data/web/inc/lib/sieve/extensions/imapflags.xml | 21 +
data/web/inc/lib/sieve/extensions/index.xml | 17 +
data/web/inc/lib/sieve/extensions/notify.xml | 29 +
data/web/inc/lib/sieve/extensions/regex.xml | 11 +
data/web/inc/lib/sieve/extensions/reject.xml | 11 +
data/web/inc/lib/sieve/extensions/relational.xml | 14 +
data/web/inc/lib/sieve/extensions/spamtest.xml | 11 +
data/web/inc/lib/sieve/extensions/spamtestplus.xml | 12 +
data/web/inc/lib/sieve/extensions/subaddress.xml | 8 +
data/web/inc/lib/sieve/extensions/vacation.xml | 31 +
data/web/inc/lib/sieve/extensions/variables.xml | 21 +
data/web/inc/lib/sieve/extensions/virustest.xml | 11 +
data/web/inc/lib/sieve/keywords.xml | 91 +++
data/web/inc/prerequisites.inc.php | 4 +
data/web/inc/sessions.inc.php | 2 +
data/web/inc/vars.inc.php | 3 +
data/web/js/admin.js | 778 ++++++++-------------
data/web/js/api.js | 13 +-
data/web/js/edit.js | 1 +
data/web/js/mailbox.js | 135 +++-
data/web/js/numberedtextarea.min.js | 1 +
data/web/js/user.js | 47 +-
data/web/json_api.php | 219 +++++-
data/web/lang/lang.de.php | 24 +-
data/web/lang/lang.en.php | 20 +
data/web/mailbox.php | 36 +-
data/web/modals/mailbox.php | 77 +-
data/web/modals/user.php | 23 +-
data/web/user.php | 7 +
65 files changed, 3451 insertions(+), 700 deletions(-)
delete mode 100644 data/web/api.php
create mode 100644 data/web/css/numberedtextarea.min.css
create mode 100644 data/web/inc/ajax/sieve_validation.php
create mode 100644 data/web/inc/ajax/sogo_ctrl.php
create mode 100644 data/web/inc/ajax/syncjob_logs.php
rename data/web/inc/{call_sogo_ctrl.php => functions.docker.inc.php} (63%)
create mode 100644 data/web/inc/lib/sieve/SieveDumpable.php
create mode 100644 data/web/inc/lib/sieve/SieveException.php
create mode 100644 data/web/inc/lib/sieve/SieveKeywordRegistry.php
create mode 100644 data/web/inc/lib/sieve/SieveParser.php
create mode 100644 data/web/inc/lib/sieve/SieveScanner.php
create mode 100644 data/web/inc/lib/sieve/SieveScript.php
create mode 100644 data/web/inc/lib/sieve/SieveSemantics.php
create mode 100644 data/web/inc/lib/sieve/SieveToken.php
create mode 100644 data/web/inc/lib/sieve/SieveTree.php
create mode 100644 data/web/inc/lib/sieve/extensions/body.xml
create mode 100644 data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
create mode 100644 data/web/inc/lib/sieve/extensions/copy.xml
create mode 100644 data/web/inc/lib/sieve/extensions/date.xml
create mode 100644 data/web/inc/lib/sieve/extensions/editheader.xml
create mode 100644 data/web/inc/lib/sieve/extensions/envelope.xml
create mode 100644 data/web/inc/lib/sieve/extensions/environment.xml
create mode 100644 data/web/inc/lib/sieve/extensions/ereject.xml
create mode 100644 data/web/inc/lib/sieve/extensions/fileinto.xml
create mode 100644 data/web/inc/lib/sieve/extensions/imap4flags.xml
create mode 100644 data/web/inc/lib/sieve/extensions/imapflags.xml
create mode 100644 data/web/inc/lib/sieve/extensions/index.xml
create mode 100644 data/web/inc/lib/sieve/extensions/notify.xml
create mode 100644 data/web/inc/lib/sieve/extensions/regex.xml
create mode 100644 data/web/inc/lib/sieve/extensions/reject.xml
create mode 100644 data/web/inc/lib/sieve/extensions/relational.xml
create mode 100644 data/web/inc/lib/sieve/extensions/spamtest.xml
create mode 100644 data/web/inc/lib/sieve/extensions/spamtestplus.xml
create mode 100644 data/web/inc/lib/sieve/extensions/subaddress.xml
create mode 100644 data/web/inc/lib/sieve/extensions/vacation.xml
create mode 100644 data/web/inc/lib/sieve/extensions/variables.xml
create mode 100644 data/web/inc/lib/sieve/extensions/virustest.xml
create mode 100644 data/web/inc/lib/sieve/keywords.xml
create mode 100644 data/web/js/numberedtextarea.min.js
Diff
diff --git a/data/web/admin.php b/data/web/admin.php
index a2ec3d94..06af3db9 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -457,12 +457,11 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-postfix-logs">
<div class="panel panel-default">
- <div class="panel-heading">Postfix
+ <div class="panel-heading">Postfix <span class="badge badge-info log-lines"></span>
<div class="btn-group pull-right">
- <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
- <ul class="dropdown-menu">
- <li><a href="#" id="refresh_postfix_log"><?=$lang['admin']['refresh'];?></a></li>
- </ul>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="100">+ 100</button>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="1000">+ 1000</button>
+ <button class="btn btn-xs btn-default" id="refresh_postfix_log"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
@@ -475,12 +474,11 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
<div class="panel panel-default">
- <div class="panel-heading">Dovecot
+ <div class="panel-heading">Dovecot <span class="badge badge-info log-lines"></span>
<div class="btn-group pull-right">
- <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
- <ul class="dropdown-menu">
- <li><a href="#" id="refresh_dovecot_log"><?=$lang['admin']['refresh'];?></a></li>
- </ul>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="100">+ 100</button>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="1000">+ 1000</button>
+ <button class="btn btn-xs btn-default" id="refresh_dovecot_log"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
@@ -493,12 +491,11 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-sogo-logs">
<div class="panel panel-default">
- <div class="panel-heading">SOGo
+ <div class="panel-heading">SOGo <span class="badge badge-info log-lines"></span>
<div class="btn-group pull-right">
- <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
- <ul class="dropdown-menu">
- <li><a href="#" id="refresh_sogo_log"><?=$lang['admin']['refresh'];?></a></li>
- </ul>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="100">+ 100</button>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="1000">+ 1000</button>
+ <button class="btn btn-xs btn-default" id="refresh_sogo_log"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
@@ -511,12 +508,11 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-fail2ban-logs">
<div class="panel panel-default">
- <div class="panel-heading">Fail2ban
+ <div class="panel-heading">Fail2ban <span class="badge badge-info log-lines"></span>
<div class="btn-group pull-right">
- <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
- <ul class="dropdown-menu">
- <li><a href="#" id="refresh_fail2ban_log"><?=$lang['admin']['refresh'];?></a></li>
- </ul>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="100">+ 100</button>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="1000">+ 1000</button>
+ <button class="btn btn-xs btn-default" id="refresh_fail2ban_log"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
@@ -529,17 +525,16 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-rspamd-history">
<div class="panel panel-default">
- <div class="panel-heading">Rspamd history
+ <div class="panel-heading">Rspamd history <span class="badge badge-info log-lines"></span>
<div class="btn-group pull-right">
- <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
- <ul class="dropdown-menu">
- <li><a href="#" id="refresh_rspamd_history"><?=$lang['admin']['refresh'];?></a></li>
- </ul>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="100">+ 100</button>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="1000">+ 1000</button>
+ <button class="btn btn-xs btn-default" id="refresh_rspamd_history"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
- <table class="table table-striped table-condensed" id="rspamd_history"></table>
+ <table class="table table-striped table-condensed log-table" id="rspamd_history"></table>
</div>
</div>
</div>
@@ -547,12 +542,11 @@ $tfa_data = get_tfa();
<div role="tabpanel" class="tab-pane" id="tab-autodiscover-logs">
<div class="panel panel-default">
- <div class="panel-heading">Autodiscover
+ <div class="panel-heading">Autodiscover <span class="badge badge-info log-lines"></span>
<div class="btn-group pull-right">
- <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
- <ul class="dropdown-menu">
- <li><a href="#" id="refresh_autodiscover_log"><?=$lang['admin']['refresh'];?></a></li>
- </ul>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="100">+ 100</button>
+ <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="1000">+ 1000</button>
+ <button class="btn btn-xs btn-default" id="refresh_autodiscover_log"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
diff --git a/data/web/api.php b/data/web/api.php
deleted file mode 100644
index d250929d..00000000
--- a/data/web/api.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-set_time_limit (0);
-
-$address = '0.0.0.0';
-
-$port = 7777;
-$con = 1;
-$word = "";
-
-$sock = socket_create(AF_INET, SOCK_STREAM, 0);
-$bind = socket_bind($sock, $address, $port);
-
-socket_listen($sock);
-
-while ($con == 1)
-{
- $client = socket_accept($sock);
- $input = socket_read($client, 2024);
-
- if ($input == 'exit')
- {
- $close = socket_close($sock);
- $con = 0;
- }
-
- if($con == 1)
- {
- $word .= $input;
- }
-}
-
-echo $word;
diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php
index 5e0ccf30..2f5fb2c9 100644
--- a/data/web/autodiscover.php
+++ b/data/web/autodiscover.php
@@ -35,7 +35,8 @@ $opt = [
];
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
-$login_role = check_login($login_user, $_SERVER['PHP_AUTH_PW']);
+$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
+$login_role = check_login($login_user, $login_pass);
if (!isset($_SERVER['PHP_AUTH_USER']) OR $login_role !== "user") {
try {
diff --git a/data/web/css/footable.bootstrap.min.css b/data/web/css/footable.bootstrap.min.css
index a030a7ff..00e96c91 100644
--- a/data/web/css/footable.bootstrap.min.css
+++ b/data/web/css/footable.bootstrap.min.css
@@ -1 +1,4 @@
-table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block}
\ No newline at end of file
+table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block}
+table > tbody > tr > td > span.footable-toggle {
+ opacity: 0.7;
+}
\ No newline at end of file
diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css
index 9f07debe..5004cda1 100644
--- a/data/web/css/mailbox.css
+++ b/data/web/css/mailbox.css
@@ -34,4 +34,4 @@ table.footable>tbody>tr.footable-empty>td {
}
.inputMissingAttr {
border-color: #FF4136;
-}
+}
\ No newline at end of file
diff --git a/data/web/css/numberedtextarea.min.css b/data/web/css/numberedtextarea.min.css
new file mode 100644
index 00000000..c147b16b
--- /dev/null
+++ b/data/web/css/numberedtextarea.min.css
@@ -0,0 +1 @@
+div.numberedtextarea-wrapper{position:relative}div.numberedtextarea-wrapper textarea{display:block;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}div.numberedtextarea-line-numbers{position:absolute;top:0;left:0;right:0;bottom:0;width:50px;border-right:none;color:rgba(0,0,0,.4);overflow:hidden}div.numberedtextarea-number{padding-right:6px;text-align:right}textarea#script_data{font-family:Monospace}
\ No newline at end of file
diff --git a/data/web/css/user.css b/data/web/css/user.css
index 07d4e745..e24bebe4 100644
--- a/data/web/css/user.css
+++ b/data/web/css/user.css
@@ -30,3 +30,10 @@ table.footable>tbody>tr.footable-empty>td {
.inputMissingAttr {
border-color: #FF4136;
}
+#logText {
+ white-space: pre-wrap;
+ white-space: -moz-pre-wrap;
+ white-space: -pre-wrap;
+ white-space: -o-pre-wrap;
+ word-wrap: break-word;
+}
\ No newline at end of file
diff --git a/data/web/edit.php b/data/web/edit.php
index fd6682ff..e06a0229 100644
--- a/data/web/edit.php
+++ b/data/web/edit.php
@@ -484,19 +484,25 @@ if (isset($_SESSION['mailcow_cc_role'])) {
</form>
<hr>
<form data-id="mboxratelimit" class="form-inline well" method="post">
- <div class="form-group">
- <label class="control-label">Ratelimit</label>
- <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
- </div>
- <div class="form-group">
- <select name="rl_frame" id="rl_frame" class="form-control">
- <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
- <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
- <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
- </select>
- </div>
- <div class="form-group">
- <button class="btn btn-default" id="edit_selected" data-id="mboxratelimit" data-item="<?=$mailbox;?>" data-api-url='edit/ratelimit' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+ <div class="row">
+ <div class="col-sm-1">
+ <p class="help-block">Ratelimit</p>
+ </div>
+ <div class="col-sm-10">
+ <div class="form-group">
+ <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
+ </div>
+ <div class="form-group">
+ <select name="rl_frame" id="rl_frame" class="form-control">
+ <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
+ <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
+ <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <button class="btn btn-default" id="edit_selected" data-id="mboxratelimit" data-item="<?=$mailbox;?>" data-api-url='edit/ratelimit' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+ </div>
+ </div>
</div>
</form>
<?php
@@ -717,6 +723,59 @@ if (isset($_SESSION['mailcow_cc_role'])) {
}
}
}
+ if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin" || $_SESSION['mailcow_cc_role'] == "user") {
+ if (isset($_GET['filter']) &&
+ is_numeric($_GET['filter'])) {
+ $id = $_GET["filter"];
+ $result = mailbox('get', 'filter_details', $id);
+ if (!empty($result)) {
+ ?>
+ <h4>Filter</h4>
+ <form class="form-horizontal" data-id="editfilter" role="form" method="post">
+ <input type="hidden" value="0" name="active">
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="script_desc"><?=$lang['edit']['sieve_desc'];?>:</label>
+ <div class="col-sm-10">
+ <input type="text" class="form-control" name="script_desc" id="script_desc" value="<?=htmlspecialchars($result['script_desc'], ENT_QUOTES, 'UTF-8');?>" required maxlength="255">
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="filter_type"><?=$lang['edit']['sieve_type'];?>:</label>
+ <div class="col-sm-10">
+ <select id="addFilterType" name="filter_type" id="filter_type" required>
+ <option value="prefilter" <?=($result['filter_type'] == 'prefilter') ? 'selected' : null;?>>Prefilter</option>
+ <option value="postfilter" <?=($result['filter_type'] == 'postfilter') ? 'selected' : null;?>>Postfilter</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="script_data">Script:</label>
+ <div class="col-sm-10">
+ <textarea spellcheck="false" autocorrect="off" autocapitalize="none" class="form-control" rows="20" id="script_data" name="script_data" required><?=$result['script_data'];?></textarea>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10">
+ <div class="checkbox">
+ <label><input type="checkbox" value="1" name="active" <?=($result['active_int']=="1") ? "checked" : "";?>> <?=$lang['edit']['active'];?></label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10">
+ <button class="btn btn-success" id="edit_selected" data-id="editfilter" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/filter' data-api-attr='{}' href="#"><?=$lang['edit']['validate_save'];?></button>
+ </div>
+ </div>
+ </form>
+ <?php
+ }
+ else {
+ ?>
+ <div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+ <?php
+ }
+ }
+ }
}
else {
?>
diff --git a/data/web/inc/ajax/sieve_validation.php b/data/web/inc/ajax/sieve_validation.php
new file mode 100644
index 00000000..d24a95dc
--- /dev/null
+++ b/data/web/inc/ajax/sieve_validation.php
@@ -0,0 +1,23 @@
+<?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['script'])) {
+ $sieve = new Sieve\SieveParser();
+ try {
+ if (empty($_GET['script'])) {
+ echo json_encode(array('type' => 'danger', 'msg' => 'Script cannot be empty'));
+ exit();
+ }
+ $sieve->parse($_GET['script']);
+ }
+ catch (Exception $e) {
+ echo json_encode(array('type' => 'danger', 'msg' => $e->getMessage()));
+ exit();
+ }
+ echo json_encode(array('type' => 'success', 'msg' => $lang['add']['validation_success']));
+}
+?>
diff --git a/data/web/inc/ajax/sogo_ctrl.php b/data/web/inc/ajax/sogo_ctrl.php
new file mode 100644
index 00000000..e238d9c0
--- /dev/null
+++ b/data/web/inc/ajax/sogo_ctrl.php
@@ -0,0 +1,39 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+header('Content-Type: text/html; charset=utf-8');
+if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
+ exit();
+}
+
+if ($_GET['ACTION'] == "start") {
+ $retry = 0;
+ while (docker('sogo-mailcow', 'info')['State']['Running'] != 1 && $retry <= 3) {
+ $response = docker('sogo-mailcow', 'post', 'start');
+ $response = json_decode($response, true);
+ $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+ if ($response['type'] == "success") {
+ break;
+ }
+ usleep(1500000);
+ $retry++;
+ }
+ echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
+}
+
+if ($_GET['ACTION'] == "stop") {
+ $retry = 0;
+ while (docker('sogo-mailcow', 'info')['State']['Running'] == 1 && $retry <= 3) {
+ $response = docker('sogo-mailcow', 'post', 'stop');
+ $response = json_decode($response, true);
+ $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+ if ($response['type'] == "success") {
+ break;
+ }
+ usleep(1500000);
+ $retry++;
+ }
+ echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
+}
+
+?>
diff --git a/data/web/inc/ajax/syncjob_logs.php b/data/web/inc/ajax/syncjob_logs.php
new file mode 100644
index 00000000..a0568167
--- /dev/null
+++ b/data/web/inc/ajax/syncjob_logs.php
@@ -0,0 +1,15 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+header('Content-Type: text/plain');
+if (!isset($_SESSION['mailcow_cc_role'])) {
+ exit();
+}
+
+if (isset($_GET['id']) && is_numeric($_GET['id'])) {
+ if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
+ echo (empty($details['log'])) ? '-' : $details['log'];
+ }
+}
+
+?>
diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php
index 8740612c..9246d230 100644
--- a/data/web/inc/footer.inc.php
+++ b/data/web/inc/footer.inc.php
@@ -8,6 +8,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/footer.php';
<script src="/js/bootstrap-select.min.js"></script>
<script src="/js/bootstrap-filestyle.min.js"></script>
<script src="/js/notifications.min.js"></script>
+<script src="/js/numberedtextarea.min.js"></script>
<script src="/js/u2f-api.js"></script>
<script src="/js/api.js"></script>
<script>
@@ -21,12 +22,17 @@ function setLang(sel) {
}
$(document).ready(function() {
- function mailcow_alert_box(message, type) {
+ window.mailcow_alert_box = function(message, type) {
msg = $('<span/>').html(message).text();
- $.notify({message: msg},{type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
+ if (type == 'danger') {
+ auto_hide = 0;
+ } else {
+ auto_hide = 5000;
+ }
+ $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
}
<?php if (isset($_SESSION['return'])): ?>
- mailcow_alert_box("<?= $_SESSION['return']['msg']; ?>", "<?= $_SESSION['return']['type']; ?>");
+ mailcow_alert_box(<?=json_encode($_SESSION['return']['msg']); ?>, "<?= $_SESSION['return']['type']; ?>");
<?php endif; unset($_SESSION['return']); ?>
// Confirm TFA modal
<?php if (isset($_SESSION['pending_tfa_method'])):?>
@@ -170,7 +176,7 @@ $(document).ready(function() {
$('#statusTriggerRestartSogo').text('Stopping SOGo workers, this may take a while... ');
$.ajax({
method: 'get',
- url: '/inc/call_sogo_ctrl.php',
+ url: '/inc/ajax/sogo_ctrl.php',
data: {
'ajax': true,
'ACTION': 'stop'
@@ -180,7 +186,7 @@ $(document).ready(function() {
$('#statusTriggerRestartSogo').append('<br>Starting SOGo...');
$.ajax({
method: 'get',
- url: '/inc/call_sogo_ctrl.php',
+ url: '/inc/ajax/sogo_ctrl.php',
data: {
'ajax': true,
'ACTION': 'start'
diff --git a/data/web/inc/call_sogo_ctrl.php b/data/web/inc/functions.docker.inc.php
similarity index 63%
rename from data/web/inc/call_sogo_ctrl.php
rename to data/web/inc/functions.docker.inc.php
index c54a0a5a..a5f2581c 100644
--- a/data/web/inc/call_sogo_ctrl.php
+++ b/data/web/inc/functions.docker.inc.php
@@ -1,11 +1,4 @@
<?php
-session_start();
-$AuthUsers = array("admin");
-if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
- echo "Not allowed." . PHP_EOL;
- exit();
-}
-
function docker($service_name, $action, $post_action = null, $post_fields = null) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER,array( 'Content-Type: application/json' ));
@@ -88,34 +81,4 @@ function docker($service_name, $action, $post_action = null, $post_fields = null
}
break;
}
-}
-
-if ($_GET['ACTION'] == "start") {
- $retry = 0;
- while (docker('sogo-mailcow', 'info')['State']['Running'] != 1 && $retry <= 3) {
- $response = docker('sogo-mailcow', 'post', 'start');
- $last_response = (trim($response) == "\"OK\"") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response . '</span></b>';
- if (trim($response) == "\"OK\"") {
- break;
- }
- usleep(1500000);
- $retry++;
- }
- echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
-}
-
-if ($_GET['ACTION'] == "stop") {
- $retry = 0;
- while (docker('sogo-mailcow', 'info')['State']['Running'] == 1 && $retry <= 3) {
- $response = docker('sogo-mailcow', 'post', 'stop');
- $last_response = (trim($response) == "\"OK\"") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response . '</span></b>';
- if (trim($response) == "\"OK\"") {
- break;
- }
- usleep(1500000);
- $retry++;
- }
- echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
-}
-
-?>
+}
\ No newline at end of file
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 79a449eb..1ba9e644 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -878,15 +878,24 @@ function get_u2f_registrations($username) {
$sel->execute(array($username));
return $sel->fetchAll(PDO::FETCH_OBJ);
}
-function get_logs($container, $lines = 100) {
+function get_logs($container, $lines = false) {
+ if ($lines === false) {
+ $lines = $GLOBALS['LOG_LINES'] - 1;
+ }
global $lang;
global $redis;
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
- $lines = intval($lines);
if ($container == "dovecot-mailcow") {
- if ($data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines)) {
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ $data = $redis->lRange('DOVECOT_MAILLOG', intval($from), intval($to));
+ }
+ else {
+ $data = $redis->lRange('DOVECOT_MAILLOG', 0, intval($lines));
+ }
+ if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
@@ -894,7 +903,14 @@ function get_logs($container, $lines = 100) {
}
}
if ($container == "postfix-mailcow") {
- if ($data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines)) {
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ $data = $redis->lRange('POSTFIX_MAILLOG', intval($from), intval($to));
+ }
+ else {
+ $data = $redis->lRange('POSTFIX_MAILLOG', 0, intval($lines));
+ }
+ if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
@@ -902,7 +918,14 @@ function get_logs($container, $lines = 100) {
}
}
if ($container == "sogo-mailcow") {
- if ($data = $redis->lRange('SOGO_LOG', 0, $lines)) {
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ $data = $redis->lRange('SOGO_LOG', intval($from), intval($to));
+ }
+ else {
+ $data = $redis->lRange('SOGO_LOG', 0, intval($lines));
+ }
+ if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
@@ -910,7 +933,14 @@ function get_logs($container, $lines = 100) {
}
}
if ($container == "fail2ban-mailcow") {
- if ($data = $redis->lRange('F2B_LOG', 0, $lines)) {
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ $data = $redis->lRange('F2B_LOG', intval($from), intval($to));
+ }
+ else {
+ $data = $redis->lRange('F2B_LOG', 0, intval($lines));
+ }
+ if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
@@ -918,7 +948,14 @@ function get_logs($container, $lines = 100) {
}
}
if ($container == "autodiscover-mailcow") {
- if ($data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines)) {
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ $data = $redis->lRange('AUTODISCOVER_LOG', intval($from), intval($to));
+ }
+ else {
+ $data = $redis->lRange('AUTODISCOVER_LOG', 0, intval($lines));
+ }
+ if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
@@ -927,10 +964,16 @@ function get_logs($container, $lines = 100) {
}
if ($container == "rspamd-history") {
$curl = curl_init();
- curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history");
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history?from=" . intval($from) . "&to=" . intval($to));
+ }
+ else {
+ curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history?to=" . intval($lines));
+ }
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$history = curl_exec($curl);
- if (!curl_errno($ch)) {
+ if (!curl_errno($curl)) {
$data_array = json_decode($history, true);
curl_close($curl);
return $data_array['rows'];
@@ -940,4 +983,4 @@ function get_logs($container, $lines = 100) {
}
return false;
}
-?>
\ No newline at end of file
+?>
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 209b4acc..ed3caaa7 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -1,5 +1,5 @@
<?php
-function mailbox($_action, $_type, $_data = null) {
+function mailbox($_action, $_type, $_data = null, $attr = null) {
global $pdo;
global $redis;
global $lang;
@@ -72,6 +72,116 @@ function mailbox($_action, $_type, $_data = null) {
'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($usernames))
);
break;
+ case 'filter':
+ $sieve = new Sieve\SieveParser();
+ if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
+ if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ else {
+ $username = $_data['username'];
+ }
+ }
+ elseif ($_SESSION['mailcow_cc_role'] == "user") {
+ $username = $_SESSION['mailcow_cc_username'];
+ }
+ else {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'No user defined'
+ );
+ return false;
+ }
+ $active = intval($_data['active']);
+ $script_data = $_data['script_data'];
+ $script_desc = $_data['script_desc'];
+ $filter_type = $_data['filter_type'];
+ if (empty($script_data)) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Script cannot be empty'
+ );
+ return false;
+ }
+ try {
+ $sieve->parse($script_data);
+ }
+ catch (Exception $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Sieve parser error: ' . $e->getMessage()
+ );
+ return false;
+ }
+ if (empty($script_data) || empty($script_desc)) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Please define values for all fields'
+ );
+ return false;
+ }
+ if ($filter_type != 'postfilter' && $filter_type != 'prefilter') {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Wrong filter type'
+ );
+ return false;
+ }
+ if (!empty($active)) {
+ $script_name = 'active';
+ try {
+ $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_name` = 'inactive' WHERE `username` = :username AND `filter_type` = :filter_type");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':filter_type' => $filter_type
+ ));
+ }
+ catch(PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ return false;
+ }
+ }
+ else {
+ $script_name = 'inactive';
+ }
+ try {
+ $stmt = $pdo->prepare("INSERT INTO `sieve_filters` (`username`, `script_data`, `script_desc`, `script_name`, `filter_type`)
+ VALUES (:username, :script_data, :script_desc, :script_name, :filter_type)");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':script_data' => $script_data,
+ ':script_desc' => $script_desc,
+ ':script_name' => $script_name,
+ ':filter_type' => $filter_type
+ ));
+ }
+ catch(PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ return false;
+ }
+ $_SESSION['return'] = array(
+ 'type' => 'success',
+ 'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+ );
+ return true;
+ break;
case 'syncjob':
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'] = array(
@@ -1379,6 +1489,100 @@ function mailbox($_action, $_type, $_data = null) {
);
return true;
break;
+ case 'filter':
+ $sieve = new Sieve\SieveParser();
+ if (!is_array($_data['id'])) {
+ $ids = array();
+ $ids[] = $_data['id'];
+ }
+ else {
+ $ids = $_data['id'];
+ }
+ if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ foreach ($ids as $id) {
+ $is_now = mailbox('get', 'filter_details', $id);
+ if (!empty($is_now)) {
+ $username = $is_now['username'];
+ $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+ $script_desc = (!empty($_data['script_desc'])) ? $_data['script_desc'] : $is_now['script_desc'];
+ $script_data = (!empty($_data['script_data'])) ? $_data['script_data'] : $is_now['script_data'];
+ $filter_type = (!empty($_data['filter_type'])) ? $_data['filter_type'] : $is_now['filter_type'];
+ }
+ else {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ try {
+ $sieve->parse($script_data);
+ }
+ catch (Exception $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Sieve parser error: ' . $e->getMessage()
+ );
+ return false;
+ }
+ if ($filter_type != 'postfilter' && $filter_type != 'prefilter') {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Wrong filter type'
+ );
+ return false;
+ }
+ if ($active == '1') {
+ $script_name = 'active';
+ try {
+ $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_name` = 'inactive' WHERE `username` = :username AND `filter_type` = :filter_type");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':filter_type' => $filter_type
+ ));
+ }
+ catch(PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ return false;
+ }
+ }
+ else {
+ $script_name = 'inactive';
+ }
+ try {
+ $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_desc` = :script_desc, `script_data` = :script_data, `script_name` = :script_name, `filter_type` = :filter_type
+ WHERE `id` = :id");
+ $stmt->execute(array(
+ ':script_desc' => $script_desc,
+ ':script_data' => $script_data,
+ ':script_name' => $script_name,
+ ':filter_type' => $filter_type,
+ ':id' => $id
+ ));
+ }
+ catch(PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ return false;
+ }
+ }
+ $_SESSION['return'] = array(
+ 'type' => 'success',
+ 'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+ );
+ return true;
+ break;
case 'alias':
if (!is_array($_data['address'])) {
$addresses = array();
@@ -2140,19 +2344,128 @@ function mailbox($_action, $_type, $_data = null) {
}
return $policydata;
break;
+ case 'filters':
+ $filters = array();
+ if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
+ if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+ return false;
+ }
+ }
+ else {
+ $_data = $_SESSION['mailcow_cc_username'];
+ }
+ try {
+ $stmt = $pdo->prepare("SELECT `id` FROM `sieve_filters` WHERE `username` = :username");
+ $stmt->execute(array(':username' => $_data));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $filters[] = $row['id'];
+ }
+ }
+ catch(PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ }
+ return $filters;
+ break;
+ case 'filter_details':
+ $filter_details = array();
+ if (!is_numeric($_data)) {
+ return false;
+ }
+ try {
+ $stmt = $pdo->prepare("SELECT CASE `script_name` WHEN 'active' THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+ CASE `script_name` WHEN 'active' THEN 1 ELSE 0 END AS `active_int`,
+ id,
+ username,
+ filter_type,
+ script_data,
+ script_desc
+ FROM `sieve_filters`
+ WHERE `id` = :id");
+ $stmt->execute(array(':id' => $_data));
+ $filter_details = $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+ catch(PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ return false;
+ }
+ if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $filter_details['username'])) {
+ return false;
+ }
+ return $filter_details;
+ break;
+ case 'active_user_sieve':
+ $filter_details = array();
+ if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
+ if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+ return false;
+ }
+ }
+ else {
+ $_data = $_SESSION['mailcow_cc_username'];
+ }
+ $exec_fields = array(
+ 'cmd' => 'sieve_list',
+ 'username' => $_data
+ );
+ $filters = json_decode(docker('dovecot-mailcow', 'post', 'exec', $exec_fields), true);
+ $filters = array_filter(explode(PHP_EOL, $filters));
+ foreach ($filters as $filter) {
+ if (preg_match('/.+ ACTIVE/i', $filter)) {
+ $exec_fields = array(
+ 'cmd' => 'sieve_print',
+ 'script_name' => substr($filter, 0, -7),
+ 'username' => $_data
+ );
+ $filters = json_decode(docker('dovecot-mailcow', 'post', 'exec', $exec_fields), true);
+ return preg_replace('/^.+\n/', '', $filters);
+ }
+ }
+ return false;
+ break;
case 'syncjob_details':
$syncjobdetails = array();
if (!is_numeric($_data)) {
return false;
}
try {
- $stmt = $pdo->prepare("SELECT *,
- CONCAT(LEFT(`password1`, 3), '...') AS `password1_short`,
- `active` AS `active_int`,
- CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
- FROM `imapsync` WHERE id = :id");
+ if (isset($attr) && in_array('no_log', $attr)) {
+ $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")');
+ $fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
+ while($field = array_shift($fields)) {
+ $shown_fields[] = $field['Field'];
+ }
+ $stmt = $pdo->prepare("SELECT " . implode(',', $shown_fields) . ",
+ `active` AS `active_int`,
+ CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+ FROM `imapsync` WHERE id = :id");
+ }
+ else {
+ $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")');
+ $fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
+ while($field = array_shift($fields)) {
+ $shown_fields[] = $field['Field'];
+ }
+ $stmt = $pdo->prepare("SELECT " . implode(',', $shown_fields) . ",
+ `active` AS `active_int`,
+ CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+ FROM `imapsync` WHERE id = :id");
+ }
$stmt->execute(array(':id' => $_data));
$syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (!empty($syncjobdetails['returned_text'])) {
+ $syncjobdetails['log'] = $syncjobdetails['returned_text'];
+ }
+ else {
+ $syncjobdetails['log'] = '';
+ }
+ unset($syncjobdetails['returned_text']);
}
catch(PDOException $e) {
$_SESSION['return'] = array(
@@ -2816,6 +3129,57 @@ function mailbox($_action, $_type, $_data = null) {
);
return true;
break;
+ case 'filter':
+ if (!is_array($_data['id'])) {
+ $ids = array();
+ $ids[] = $_data['id'];
+ }
+ else {
+ $ids = $_data['id'];
+ }
+ if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ foreach ($ids as $id) {
+ if (!is_numeric($id)) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => $id
+ );
+ return false;
+ }
+ try {
+ $stmt = $pdo->prepare("SELECT `username` FROM `sieve_filters` WHERE id = :id");
+ $stmt->execute(array(':id' => $id));
+ $usr = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
+ if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $usr)) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ $stmt = $pdo->prepare("DELETE FROM `sieve_filters` WHERE `id`= :id");
+ $stmt->execute(array(':id' => $id));
+ }
+ catch (PDOException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'MySQL: '.$e
+ );
+ return false;
+ }
+ }
+ $_SESSION['return'] = array(
+ 'type' => 'success',
+ 'msg' => 'Deleted filter id/s ' . implode(', ', $ids)
+ );
+ return true;
+ break;
case 'time_limited_alias':
if (!is_array($_data['address'])) {
$addresses = array();
diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php
index b64bd5a8..cedd07ca 100644
--- a/data/web/inc/header.inc.php
+++ b/data/web/inc/header.inc.php
@@ -22,6 +22,7 @@
<link rel="stylesheet" href="/inc/languages.min.css">
<link rel="stylesheet" href="/css/mailcow.css">
<link rel="stylesheet" href="/css/animate.min.css">
+<link rel="stylesheet" href="/css/numberedtextarea.min.css">
<?= (preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null; ?>
<?= (preg_match("/admin.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null; ?>
<?= (preg_match("/user.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/user.css">' : null; ?>
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 16f3fdf2..9b6a63c9 100644
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
- $db_version = "25102017_0748";
+ $db_version = "31102017_1049";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -27,7 +27,13 @@ function init_db_schema() {
GROUP BY logged_in_as;",
"grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
- LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;"
+ LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;",
+ "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
+ SELECT id, username, script_name, script_data FROM sieve_filters
+ WHERE filter_type = 'prefilter';",
+ "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS
+ SELECT id, username, script_name, script_data FROM sieve_filters
+ WHERE filter_type = 'postfilter';"
);
$tables = array(
@@ -155,6 +161,36 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
+ "sieve_filters" => array(
+ "cols" => array(
+ "id" => "INT NOT NULL AUTO_INCREMENT",
+ "username" => "VARCHAR(255) NOT NULL",
+ "script_desc" => "VARCHAR(255) NOT NULL",
+ "script_name" => "ENUM('active','inactive')",
+ "script_data" => "TEXT NOT NULL",
+ "filter_type" => "ENUM('postfilter','prefilter')",
+ "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+ "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
+ ),
+ "keys" => array(
+ "primary" => array(
+ "" => array("id")
+ ),
+ "key" => array(
+ "username" => array("username"),
+ "script_desc" => array("script_desc")
+ ),
+ "fkey" => array(
+ "fk_username_sieve_global_before" => array(
+ "col" => "username",
+ "ref" => "mailbox.username",
+ "delete" => "CASCADE",
+ "update" => "NO ACTION"
+ )
+ )
+ ),
+ "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+ ),
"user_acl" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",
@@ -165,6 +201,7 @@ function init_db_schema() {
"delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
"syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
"eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
+ "filters" => "TINYINT(1) NOT NULL DEFAULT '1'",
),
"keys" => array(
"fkey" => array(
@@ -248,8 +285,11 @@ function init_db_schema() {
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
+ "primary" => array(
+ "" => array("username")
+ ),
"key" => array(
- "username" => array("username")
+ "domain" => array("domain")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
diff --git a/data/web/inc/lib/sieve/SieveDumpable.php b/data/web/inc/lib/sieve/SieveDumpable.php
new file mode 100644
index 00000000..af82d6af
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveDumpable.php
@@ -0,0 +1,7 @@
+<?php namespace Sieve;
+
+interface SieveDumpable
+{
+ function dump();
+ function text();
+}
diff --git a/data/web/inc/lib/sieve/SieveException.php b/data/web/inc/lib/sieve/SieveException.php
new file mode 100644
index 00000000..da7fe604
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveException.php
@@ -0,0 +1,47 @@
+<?php namespace Sieve;
+
+require_once('SieveToken.php');
+
+use Exception;
+
+class SieveException extends Exception
+{
+ protected $token_;
+
+ public function __construct(SieveToken $token, $arg)
+ {
+ $message = 'undefined sieve exception';
+ $this->token_ = $token;
+
+ if (is_string($arg))
+ {
+ $message = $arg;
+ }
+ else
+ {
+ if (is_array($arg))
+ {
+ $type = SieveToken::typeString(array_shift($arg));
+ foreach($arg as $t)
+ {
+ $type .= ' or '. SieveToken::typeString($t);
+ }
+ }
+ else
+ {
+ $type = SieveToken::typeString($arg);
+ }
+
+ $tokenType = SieveToken::typeString($token->type);
+ $message = "$tokenType where $type expected near ". $token->text;
+ }
+
+ parent::__construct('line '. $token->line .": $message");
+ }
+
+ public function getLineNo()
+ {
+ return $this->token_->line;
+ }
+
+}
diff --git a/data/web/inc/lib/sieve/SieveKeywordRegistry.php b/data/web/inc/lib/sieve/SieveKeywordRegistry.php
new file mode 100644
index 00000000..b901dbc3
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveKeywordRegistry.php
@@ -0,0 +1,233 @@
+<?php namespace Sieve;
+
+class SieveKeywordRegistry
+{
+ protected $registry_ = array();
+ protected $matchTypes_ = array();
+ protected $comparators_ = array();
+ protected $addressParts_ = array();
+ protected $commands_ = array();
+ protected $tests_ = array();
+ protected $arguments_ = array();
+
+ protected static $refcount = 0;
+ protected static $instance = null;
+
+ protected function __construct()
+ {
+ $keywords = simplexml_load_file(dirname(__FILE__) .'/keywords.xml');
+ foreach ($keywords->children() as $keyword)
+ {
+ switch ($keyword->getName())
+ {
+ case 'matchtype':
+ $type =& $this->matchTypes_;
+ break;
+ case 'comparator':
+ $type =& $this->comparators_;
+ break;
+ case 'addresspart':
+ $type =& $this->addressParts_;
+ break;
+ case 'test':
+ $type =& $this->tests_;
+ break;
+ case 'command':
+ $type =& $this->commands_;
+ break;
+ default:
+ trigger_error('Unsupported keyword type "'. $keyword->getName()
+ . '" in file "keywords/'. basename($file) .'"');
+ return;
+ }
+
+ $name = (string) $keyword['name'];
+ if (array_key_exists($name, $type))
+ trigger_error("redefinition of $type $name - skipping");
+ else
+ $type[$name] = $keyword->children();
+ }
+
+ foreach (glob(dirname(__FILE__) .'/extensions/*.xml') as $file)
+ {
+ $extension = simplexml_load_file($file);
+ $name = (string) $extension['name'];
+
+ if (array_key_exists($name, $this->registry_))
+ {
+ trigger_error('overwriting extension "'. $name .'"');
+ }
+ $this->registry_[$name] = $extension;
+ }
+ }
+
+ public static function get()
+ {
+ if (self::$instance == null)
+ {
+ self::$instance = new SieveKeywordRegistry();
+ }
+
+ self::$refcount++;
+
+ return self::$instance;
+ }
+
+ public function put()
+ {
+ if (--self::$refcount == 0)
+ {
+ self::$instance = null;
+ }
+ }
+
+ public function activate($extension)
+ {
+ if (!isset($this->registry_[$extension]))
+ {
+ return;
+ }
+
+ $xml = $this->registry_[$extension];
+
+ foreach ($xml->children() as $e)
+ {
+ switch ($e->getName())
+ {
+ case 'matchtype':
+ $type =& $this->matchTypes_;
+ break;
+ case 'comparator':
+ $type =& $this->comparators_;
+ break;
+ case 'addresspart':
+ $type =& $this->addressParts_;
+ break;
+ case 'test':
+ $type =& $this->tests_;
+ break;
+ case 'command':
+ $type =& $this->commands_;
+ break;
+ case 'tagged-argument':
+ $xml = $e->parameter[0];
+ $this->arguments_[(string) $xml['name']] = array(
+ 'extends' => (string) $e['extends'],
+ 'rules' => $xml
+ );
+ continue;
+ default:
+ trigger_error('Unsupported extension type \''.
+ $e->getName() ."' in extension '$extension'");
+ return;
+ }
+
+ $name = (string) $e['name'];
+ if (!isset($type[$name]) ||
+ (string) $e['overrides'] == 'true')
+ {
+ $type[$name] = $e->children();
+ }
+ }
+ }
+
+ public function isTest($name)
+ {
+ return (isset($this->tests_[$name]) ? true : false);
+ }
+
+ public function isCommand($name)
+ {
+ return (isset($this->commands_[$name]) ? true : false);
+ }
+
+ public function matchtype($name)
+ {
+ if (isset($this->matchTypes_[$name]))
+ {
+ return $this->matchTypes_[$name];
+ }
+ return null;
+ }
+
+ public function addresspart($name)
+ {
+ if (isset($this->addressParts_[$name]))
+ {
+ return $this->addressParts_[$name];
+ }
+ return null;
+ }
+
+ public function comparator($name)
+ {
+ if (isset($this->comparators_[$name]))
+ {
+ return $this->comparators_[$name];
+ }
+ return null;
+ }
+
+ public function test($name)
+ {
+ if (isset($this->tests_[$name]))
+ {
+ return $this->tests_[$name];
+ }
+ return null;
+ }
+
+ public function command($name)
+ {
+ if (isset($this->commands_[$name]))
+ {
+ return $this->commands_[$name];
+ }
+ return null;
+ }
+
+ public function arguments($command)
+ {
+ $res = array();
+ foreach ($this->arguments_ as $arg)
+ {
+ if (preg_match('/'.$arg['extends'].'/', $command))
+ array_push($res, $arg['rules']);
+ }
+ return $res;
+ }
+
+ public function argument($name)
+ {
+ if (isset($this->arguments_[$name]))
+ {
+ return $this->arguments_[$name]['rules'];
+ }
+ return null;
+ }
+
+ public function requireStrings()
+ {
+ return array_keys($this->registry_);
+ }
+ public function matchTypes()
+ {
+ return array_keys($this->matchTypes_);
+ }
+ public function comparators()
+ {
+ return array_keys($this->comparators_);
+ }
+ public function addressParts()
+ {
+ return array_keys($this->addressParts_);
+ }
+ public function tests()
+ {
+ return array_keys($this->tests_);
+ }
+ public function commands()
+ {
+ return array_keys($this->commands_);
+ }
+}
diff --git a/data/web/inc/lib/sieve/SieveParser.php b/data/web/inc/lib/sieve/SieveParser.php
new file mode 100644
index 00000000..3d81c14b
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveParser.php
@@ -0,0 +1,255 @@
+<?php namespace Sieve;
+
+include_once 'SieveTree.php';
+include_once 'SieveScanner.php';
+include_once 'SieveSemantics.php';
+include_once 'SieveException.php';
+
+class SieveParser
+{
+ protected $scanner_;
+ protected $script_;
+ protected $tree_;
+ protected $status_;
+
+ public function __construct($script = null)
+ {
+ if (isset($script))
+ $this->parse($script);
+ }
+
+ public function GetParseTree()
+ {
+ return $this->tree_;
+ }
+
+ public function dumpParseTree()
+ {
+ return $this->tree_->dump();
+ }
+
+ public function getScriptText()
+ {
+ return $this->tree_->getText();
+ }
+
+ protected function getPrevToken_($parent_id)
+ {
+ $childs = $this->tree_->getChilds($parent_id);
+
+ for ($i = count($childs); $i > 0; --$i)
+ {
+ $prev = $this->tree_->getNode($childs[$i-1]);
+ if ($prev->is(SieveToken::Comment|SieveToken::Whitespace))
+ continue;
+
+ // use command owning a block or list instead of previous
+ if ($prev->is(SieveToken::BlockStart|SieveToken::Comma|SieveToken::LeftParenthesis))
+ $prev = $this->tree_->getNode($parent_id);
+
+ return $prev;
+ }
+
+ return $this->tree_->getNode($parent_id);
+ }
+
+ /*******************************************************************************
+ * methods for recursive descent start below
+ */
+ public function passthroughWhitespaceComment($token)
+ {
+ return 0;
+ }
+
+ public function passthroughFunction($token)
+ {
+ $this->tree_->addChild($token);
+ }
+
+ public function parse($script)
+ {
+ $this->script_ = $script;
+
+ $this->scanner_ = new SieveScanner($this->script_);
+
+ // Define what happens with passthrough tokens like whitespacs and comments
+ $this->scanner_->setPassthroughFunc(
+ array(
+ $this, 'passthroughWhitespaceComment'
+ )
+ );
+
+ $this->tree_ = new SieveTree('tree');
+
+ $this->commands_($this->tree_->getRoot());
+
+ if (!$this->scanner_->nextTokenIs(SieveToken::ScriptEnd)) {
+ $token = $this->scanner_->nextToken();
+ throw new SieveException($token, SieveToken::ScriptEnd);
+ }
+ }
+
+ protected function commands_($parent_id)
+ {
+ while (true)
+ {
+ if (!$this->scanner_->nextTokenIs(SieveToken::Identifier))
+ break;
+
+ // Get and check a command token
+ $token = $this->scanner_->nextToken();
+ $semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
+
+ // Process eventual arguments
+ $this_node = $this->tree_->addChildTo($parent_id, $token);
+ $this->arguments_($this_node, $semantics);
+
+ $token = $this->scanner_->nextToken();
+ if (!$token->is(SieveToken::Semicolon))
+ {
+ // TODO: check if/when semcheck is needed here
+ $semantics->validateToken($token);
+
+ if ($token->is(SieveToken::BlockStart))
+ {
+ $this->tree_->addChildTo($this_node, $token);
+ $this->block_($this_node, $semantics);
+ continue;
+ }
+
+ throw new SieveException($token, SieveToken::Semicolon);
+ }
+
+ $semantics->done($token);
+ $this->tree_->addChildTo($this_node, $token);
+ }
+ }
+
+ protected function arguments_($parent_id, &$semantics)
+ {
+ while (true)
+ {
+ if ($this->scanner_->nextTokenIs(SieveToken::Number|SieveToken::Tag))
+ {
+ // Check if semantics allow a number or tag
+ $token = $this->scanner_->nextToken();
+ $semantics->validateToken($token);
+ $this->tree_->addChildTo($parent_id, $token);
+ }
+ else if ($this->scanner_->nextTokenIs(SieveToken::StringList))
+ {
+ $this->stringlist_($parent_id, $semantics);
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if ($this->scanner_->nextTokenIs(SieveToken::TestList))
+ {
+ $this->testlist_($parent_id, $semantics);
+ }
+ }
+
+ protected function stringlist_($parent_id, &$semantics)
+ {
+ if (!$this->scanner_->nextTokenIs(SieveToken::LeftBracket))
+ {
+ $this->string_($parent_id, $semantics);
+ return;
+ }
+
+ $token = $this->scanner_->nextToken();
+ $semantics->startStringList($token);
+ $this->tree_->addChildTo($parent_id, $token);
+
+ if($this->scanner_->nextTokenIs(SieveToken::RightBracket)) {
+ //allow empty lists
+ $token = $this->scanner_->nextToken();
+ $this->tree_->addChildTo($parent_id, $token);
+ $semantics->endStringList();
+ return;
+ }
+
+ do
+ {
+ $this->string_($parent_id, $semantics);
+ $token = $this->scanner_->nextToken();
+
+ if (!$token->is(SieveToken::Comma|SieveToken::RightBracket))
+ throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightBracket));
+
+ if ($token->is(SieveToken::Comma))
+ $semantics->continueStringList();
+
+ $this->tree_->addChildTo($parent_id, $token);
+ }
+ while (!$token->is(SieveToken::RightBracket));
+
+ $semantics->endStringList();
+ }
+
+ protected function string_($parent_id, &$semantics)
+ {
+ $token = $this->scanner_->nextToken();
+ $semantics->validateToken($token);
+ $this->tree_->addChildTo($parent_id, $token);
+ }
+
+ protected function testlist_($parent_id, &$semantics)
+ {
+ if (!$this->scanner_->nextTokenIs(SieveToken::LeftParenthesis))
+ {
+ $this->test_($parent_id, $semantics);
+ return;
+ }
+
+ $token = $this->scanner_->nextToken();
+ $semantics->validateToken($token);
+ $this->tree_->addChildTo($parent_id, $token);
+
+ do
+ {
+ $this->test_($parent_id, $semantics);
+
+ $token = $this->scanner_->nextToken();
+ if (!$token->is(SieveToken::Comma|SieveToken::RightParenthesis))
+ {
+ throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightParenthesis));
+ }
+ $this->tree_->addChildTo($parent_id, $token);
+ }
+ while (!$token->is(SieveToken::RightParenthesis));
+ }
+
+ protected function test_($parent_id, &$semantics)
+ {
+ // Check if semantics allow an identifier
+ $token = $this->scanner_->nextToken();
+ $semantics->validateToken($token);
+
+ // Get semantics for this test command
+ $this_semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
+ $this_node = $this->tree_->addChildTo($parent_id, $token);
+
+ // Consume eventual argument tokens
+ $this->arguments_($this_node, $this_semantics);
+
+ // Check that all required arguments were there
+ $token = $this->scanner_->peekNextToken();
+ $this_semantics->done($token);
+ }
+
+ protected function block_($parent_id, &$semantics)
+ {
+ $this->commands_($parent_id, $semantics);
+
+ $token = $this->scanner_->nextToken();
+ if (!$token->is(SieveToken::BlockEnd))
+ {
+ throw new SieveException($token, SieveToken::BlockEnd);
+ }
+ $this->tree_->addChildTo($parent_id, $token);
+ }
+}
diff --git a/data/web/inc/lib/sieve/SieveScanner.php b/data/web/inc/lib/sieve/SieveScanner.php
new file mode 100644
index 00000000..a0fa57a2
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveScanner.php
@@ -0,0 +1,145 @@
+<?php namespace Sieve;
+
+include_once('SieveToken.php');
+
+class SieveScanner
+{
+ public function __construct(&$script)
+ {
+ if ($script === null)
+ return;
+
+ $this->tokenize($script);
+ }
+
+ public function setPassthroughFunc($callback)
+ {
+ if ($callback == null || is_callable($callback))
+ $this->ptFn_ = $callback;
+ }
+
+ public function tokenize(&$script)
+ {
+ $pos = 0;
+ $line = 1;
+
+ $scriptLength = mb_strlen($script);
+
+ $unprocessedScript = $script;
+
+
+ //create one regex to find the right match
+ //avoids looping over all possible tokens: increases performance
+ $nameToType = [];
+ $regex = [];
+ // chr(65) == 'A'
+ $i = 65;
+
+ foreach ($this->tokenMatch_ as $type => $subregex) {
+ $nameToType[chr($i)] = $type;
+ $regex[] = "(?P<". chr($i) . ">^$subregex)";
+ $i++;
+ }
+
+ $regex = '/' . join('|', $regex) . '/';
+
+ while ($pos < $scriptLength)
+ {
+ if (preg_match($regex, $unprocessedScript, $match)) {
+
+ // only keep the group that match and we only want matches with group names
+ // we can use the group name to find the token type using nameToType
+ $filterMatch = array_filter(array_filter($match), 'is_string', ARRAY_FILTER_USE_KEY);
+
+ // the first element in filterMatch will contain the matched group and the key will be the name
+ $type = $nameToType[key($filterMatch)];
+ $currentMatch = current($filterMatch);
+
+ //create the token
+ $token = new SieveToken($type, $currentMatch, $line);
+ $this->tokens_[] = $token;
+
+ if ($type == SieveToken::Unknown)
+ return;
+
+ // just remove the part that we parsed: don't extract the new substring using script length
+ // as mb_strlen is \theta(pos) (it's linear in the position)
+ $matchLength = mb_strlen($currentMatch);
+ $unprocessedScript = mb_substr($unprocessedScript, $matchLength);
+
+ $pos += $matchLength;
+ $line += mb_substr_count($currentMatch, "\n");
+ } else {
+ $this->tokens_[] = new SieveToken(SieveToken::Unknown, '', $line);
+ return;
+ }
+
+ }
+
+ $this->tokens_[] = new SieveToken(SieveToken::ScriptEnd, '', $line);
+ }
+
+ public function nextTokenIs($type)
+ {
+ return $this->peekNextToken()->is($type);
+ }
+
+ public function peekNextToken()
+ {
+ $offset = 0;
+ do {
+ $next = $this->tokens_[$this->tokenPos_ + $offset++];
+ } while ($next->is(SieveToken::Comment|SieveToken::Whitespace));
+
+ return $next;
+ }
+
+ public function nextToken()
+ {
+ $token = $this->tokens_[$this->tokenPos_++];
+
+ while ($token->is(SieveToken::Comment|SieveToken::Whitespace))
+ {
+ if ($this->ptFn_ != null)
+ call_user_func($this->ptFn_, $token);
+
+ $token = $this->tokens_[$this->tokenPos_++];
+ }
+
+ return $token;
+ }
+
+ protected $ptFn_ = null;
+ protected $tokenPos_ = 0;
+ protected $tokens_ = array();
+ protected $tokenMatch_ = array (
+ SieveToken::LeftBracket => '\[',
+ SieveToken::RightBracket => '\]',
+ SieveToken::BlockStart => '\{',
+ SieveToken::BlockEnd => '\}',
+ SieveToken::LeftParenthesis => '\(',
+ SieveToken::RightParenthesis => '\)',
+ SieveToken::Comma => ',',
+ SieveToken::Semicolon => ';',
+ SieveToken::Whitespace => '[ \r\n\t]+',
+ SieveToken::Tag => ':[[:alpha:]_][[:alnum:]_]*(?=\b)',
+ /*
+ " # match a quotation mark
+ ( # start matching parts that include an escaped quotation mark
+ ([^"]*[^"\\\\]) # match a string without quotation marks and not ending with a backlash
+ ? # this also includes the empty string
+ (\\\\\\\\)* # match any groups of even number of backslashes
+ # (thus the character after these groups are not escaped)
+ \\\\" # match an escaped quotation mark
+ )* # accept any number of strings that end with an escaped quotation mark
+ [^"]* # accept any trailing part that does not contain any quotation marks
+ " # end of the quoted string
+ */
+ SieveToken::QuotedString => '"(([^"]*[^"\\\\])?(\\\\\\\\)*\\\\")*[^"]*"',
+ SieveToken::Number => '[[:digit:]]+(?:[KMG])?(?=\b)',
+ SieveToken::Comment => '(?:\/\*(?:[^\*]|\*(?=[^\/]))*\*\/|#[^\r\n]*\r?(\n|$))',
+ SieveToken::MultilineString => 'text:[ \t]*(?:#[^\r\n]*)?\r?\n(\.[^\r\n]+\r?\n|[^\.][^\r\n]*\r?\n)*\.\r?(\n|$)',
+ SieveToken::Identifier => '[[:alpha:]_][[:alnum:]_]*(?=\b)',
+ SieveToken::Unknown => '[^ \r\n\t]+'
+ );
+}
diff --git a/data/web/inc/lib/sieve/SieveScript.php b/data/web/inc/lib/sieve/SieveScript.php
new file mode 100644
index 00000000..befd99a1
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveScript.php
@@ -0,0 +1,6 @@
+<?php namespace Sieve;
+
+class SieveScript
+{
+ // TODO: implement
+}
diff --git a/data/web/inc/lib/sieve/SieveSemantics.php b/data/web/inc/lib/sieve/SieveSemantics.php
new file mode 100644
index 00000000..e131d5b4
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveSemantics.php
@@ -0,0 +1,611 @@
+<?php namespace Sieve;
+
+require_once('SieveKeywordRegistry.php');
+require_once('SieveToken.php');
+require_once('SieveException.php');
+
+class SieveSemantics
+{
+ protected static $requiredExtensions_ = array();
+
+ protected $comparator_;
+ protected $matchType_;
+ protected $addressPart_;
+ protected $tags_ = array();
+ protected $arguments_;
+ protected $deps_ = array();
+ protected $followupToken_;
+
+ public function __construct($token, $prevToken)
+ {
+ $this->registry_ = SieveKeywordRegistry::get();
+ $command = strtolower($token->text);
+
+ // Check the registry for $command
+ if ($this->registry_->isCommand($command))
+ {
+ $xml = $this->registry_->command($command);
+ $this->arguments_ = $this->makeArguments_($xml);
+ $this->followupToken_ = SieveToken::Semicolon;
+ }
+ else if ($this->registry_->isTest($command))
+ {
+ $xml = $this->registry_->test($command);
+ $this->arguments_ = $this->makeArguments_($xml);
+ $this->followupToken_ = SieveToken::BlockStart;
+ }
+ else
+ {
+ throw new SieveException($token, 'unknown command '. $command);
+ }
+
+ // Check if command may appear at this position within the script
+ if ($this->registry_->isTest($command))
+ {
+ if (is_null($prevToken))
+ throw new SieveException($token, $command .' may not appear as first command');
+
+ if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text))
+ throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
+ }
+ else if (isset($prevToken))
+ {
+ switch ($command)
+ {
+ case 'require':
+ $valid_after = 'require';
+ break;
+ case 'elsif':
+ case 'else':
+ $valid_after = '(if|elsif)';
+ break;
+ default:
+ $valid_after = $this->commandsRegex_();
+ }
+
+ if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text))
+ throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
+ }
+
+ // Check for extension arguments to add to the command
+ foreach ($this->registry_->arguments($command) as $arg)
+ {
+ switch ((string) $arg['type'])
+ {
+ case 'tag':
+ array_unshift($this->arguments_, array(
+ 'type' => SieveToken::Tag,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->regex_($arg),
+ 'call' => 'tagHook_',
+ 'name' => $this->name_($arg),
+ 'subArgs' => $this->makeArguments_($arg->children())
+ ));
+ break;
+ }
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->registry_->put();
+ }
+
+ // TODO: the *Regex functions could possibly also be static properties
+ protected function requireStringsRegex_()
+ {
+ return '('. implode('|', $this->registry_->requireStrings()) .')';
+ }
+
+ protected function matchTypeRegex_()
+ {
+ return '('. implode('|', $this->registry_->matchTypes()) .')';
+ }
+
+ protected function addressPartRegex_()
+ {
+ return '('. implode('|', $this->registry_->addressParts()) .')';
+ }
+
+ protected function commandsRegex_()
+ {
+ return '('. implode('|', $this->registry_->commands()) .')';
+ }
+
+ protected function testsRegex_()
+ {
+ return '('. implode('|', $this->registry_->tests()) .')';
+ }
+
+ protected function comparatorRegex_()
+ {
+ return '('. implode('|', $this->registry_->comparators()) .')';
+ }
+
+ protected function occurrence_($arg)
+ {
+ if (isset($arg['occurrence']))
+ {
+ switch ((string) $arg['occurrence'])
+ {
+ case 'optional':
+ return '?';
+ case 'any':
+ return '*';
+ case 'some':
+ return '+';
+ }
+ }
+ return '1';
+ }
+
+ protected function name_($arg)
+ {
+ if (isset($arg['name']))
+ {
+ return (string) $arg['name'];
+ }
+ return (string) $arg['type'];
+ }
+
+ protected function regex_($arg)
+ {
+ if (isset($arg['regex']))
+ {
+ return (string) $arg['regex'];
+ }
+ return '.*';
+ }
+
+ protected function case_($arg)
+ {
+ if (isset($arg['case']))
+ {
+ return (string) $arg['case'];
+ }
+ return 'adhere';
+ }
+
+ protected function follows_($arg)
+ {
+ if (isset($arg['follows']))
+ {
+ return (string) $arg['follows'];
+ }
+ return '.*';
+ }
+
+ protected function makeValue_($arg)
+ {
+ if (isset($arg->value))
+ {
+ $res = $this->makeArguments_($arg->value);
+ return array_shift($res);
+ }
+ return null;
+ }
+
+ /**
+ * Convert an extension (test) commands parameters from XML to
+ * a PHP array the {@see Semantics} class understands.
+ * @param array(SimpleXMLElement) $parameters
+ * @return array
+ */
+ protected function makeArguments_($parameters)
+ {
+ $arguments = array();
+
+ foreach ($parameters as $arg)
+ {
+ // Ignore anything not a <parameter>
+ if ($arg->getName() != 'parameter')
+ continue;
+
+ switch ((string) $arg['type'])
+ {
+ case 'addresspart':
+ array_push($arguments, array(
+ 'type' => SieveToken::Tag,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->addressPartRegex_(),
+ 'call' => 'addressPartHook_',
+ 'name' => 'address part',
+ 'subArgs' => $this->makeArguments_($arg)
+ ));
+ break;
+
+ case 'block':
+ array_push($arguments, array(
+ 'type' => SieveToken::BlockStart,
+ 'occurrence' => '1',
+ 'regex' => '{',
+ 'name' => 'block',
+ 'subArgs' => $this->makeArguments_($arg)
+ ));
+ break;
+
+ case 'comparator':
+ array_push($arguments, array(
+ 'type' => SieveToken::Tag,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => 'comparator',
+ 'name' => 'comparator',
+ 'subArgs' => array( array(
+ 'type' => SieveToken::String,
+ 'occurrence' => '1',
+ 'call' => 'comparatorHook_',
+ 'case' => 'adhere',
+ 'regex' => $this->comparatorRegex_(),
+ 'name' => 'comparator string',
+ 'follows' => 'comparator'
+ ))
+ ));
+ break;
+
+ case 'matchtype':
+ array_push($arguments, array(
+ 'type' => SieveToken::Tag,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->matchTypeRegex_(),
+ 'call' => 'matchTypeHook_',
+ 'name' => 'match type',
+ 'subArgs' => $this->makeArguments_($arg)
+ ));
+ break;
+
+ case 'number':
+ array_push($arguments, array(
+ 'type' => SieveToken::Number,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->regex_($arg),
+ 'name' => $this->name_($arg),
+ 'follows' => $this->follows_($arg)
+ ));
+ break;
+
+ case 'requirestrings':
+ array_push($arguments, array(
+ 'type' => SieveToken::StringList,
+ 'occurrence' => $this->occurrence_($arg),
+ 'call' => 'setRequire_',
+ 'case' => 'adhere',
+ 'regex' => $this->requireStringsRegex_(),
+ 'name' => $this->name_($arg)
+ ));
+ break;
+
+ case 'string':
+ array_push($arguments, array(
+ 'type' => SieveToken::String,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->regex_($arg),
+ 'case' => $this->case_($arg),
+ 'name' => $this->name_($arg),
+ 'follows' => $this->follows_($arg)
+ ));
+ break;
+
+ case 'stringlist':
+ array_push($arguments, array(
+ 'type' => SieveToken::StringList,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->regex_($arg),
+ 'case' => $this->case_($arg),
+ 'name' => $this->name_($arg),
+ 'follows' => $this->follows_($arg)
+ ));
+ break;
+
+ case 'tag':
+ array_push($arguments, array(
+ 'type' => SieveToken::Tag,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->regex_($arg),
+ 'call' => 'tagHook_',
+ 'name' => $this->name_($arg),
+ 'subArgs' => $this->makeArguments_($arg->children()),
+ 'follows' => $this->follows_($arg)
+ ));
+ break;
+
+ case 'test':
+ array_push($arguments, array(
+ 'type' => SieveToken::Identifier,
+ 'occurrence' => $this->occurrence_($arg),
+ 'regex' => $this->testsRegex_(),
+ 'name' => $this->name_($arg),
+ 'subArgs' => $this->makeArguments_($arg->children())
+ ));
+ break;
+
+ case 'testlist':
+ array_push($arguments, array(
+ 'type' => SieveToken::LeftParenthesis,
+ 'occurrence' => '1',
+ 'regex' => '\(',
+ 'name' => $this->name_($arg),
+ 'subArgs' => null
+ ));
+ array_push($arguments, array(
+ 'type' => SieveToken::Identifier,
+ 'occurrence' => '+',
+ 'regex' => $this->testsRegex_(),
+ 'name' => $this->name_($arg),
+ 'subArgs' => $this->makeArguments_($arg->children())
+ ));
+ break;
+ }
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Add argument(s) expected / allowed to appear next.
+ * @param array $value
+ */
+ protected function addArguments_($identifier, $subArgs)
+ {
+ for ($i = count($subArgs); $i > 0; $i--)
+ {
+ $arg = $subArgs[$i-1];
+ if (preg_match('/^'. $arg['follows'] .'$/si', $identifier))
+ array_unshift($this->arguments_, $arg);
+ }
+ }
+
+ /**
+ * Add dependency that is expected to be fullfilled when parsing
+ * of the current command is {@see done}.
+ * @param array $dependency
+ */
+ protected function addDependency_($type, $name, $dependencies)
+ {
+ foreach ($dependencies as $d)
+ {
+ array_push($this->deps_, array(
+ 'o_type' => $type,
+ 'o_name' => $name,
+ 'type' => $d['type'],
+ 'name' => $d['name'],
+ 'regex' => $d['regex']
+ ));
+ }
+ }
+
+ protected function invoke_($token, $func, $arg = array())
+ {
+ if (!is_array($arg))
+ $arg = array($arg);
+
+ $err = call_user_func_array(array(&$this, $func), $arg);
+
+ if ($err)
+ throw new SieveException($token, $err);
+ }
+
+ protected function setRequire_($extension)
+ {
+ array_push(self::$requiredExtensions_, $extension);
+ $this->registry_->activate($extension);
+ }
+
+ /**
+ * Hook function that is called after a address part match was found
+ * in a command. The kind of address part is remembered in case it's
+ * needed later {@see done}. For address parts from a extension
+ * dependency information and valid values are looked up as well.
+ * @param string $addresspart
+ */
+ protected function addressPartHook_($addresspart)
+ {
+ $this->addressPart_ = $addresspart;
+ $xml = $this->registry_->addresspart($this->addressPart_);
+
+ if (isset($xml))
+ {
+ // Add possible value and dependancy
+ $this->addArguments_($this->addressPart_, $this->makeArguments_($xml));
+ $this->addDependency_('address part', $this->addressPart_, $xml->requires);
+ }
+ }
+
+ /**
+ * Hook function that is called after a match type was found in a
+ * command. The kind of match type is remembered in case it's
+ * needed later {@see done}. For a match type from extensions
+ * dependency information and valid values are looked up as well.
+ * @param string $matchtype
+ */
+ protected function matchTypeHook_($matchtype)
+ {
+ $this->matchType_ = $matchtype;
+ $xml = $this->registry_->matchtype($this->matchType_);
+
+ if (isset($xml))
+ {
+ // Add possible value and dependancy
+ $this->addArguments_($this->matchType_, $this->makeArguments_($xml));
+ $this->addDependency_('match type', $this->matchType_, $xml->requires);
+ }
+ }
+
+ /**
+ * Hook function that is called after a comparator was found in
+ * a command. The comparator is remembered in case it's needed for
+ * comparsion later {@see done}. For a comparator from extensions
+ * dependency information is looked up as well.
+ * @param string $comparator
+ */
+ protected function comparatorHook_($comparator)
+ {
+ $this->comparator_ = $comparator;
+ $xml = $this->registry_->comparator($this->comparator_);
+
+ if (isset($xml))
+ {
+ // Add possible dependancy
+ $this->addDependency_('comparator', $this->comparator_, $xml->requires);
+ }
+ }
+
+ /**
+ * Hook function that is called after a tag was found in
+ * a command. The tag is remembered in case it's needed for
+ * comparsion later {@see done}. For a tags from extensions
+ * dependency information is looked up as well.
+ * @param string $tag
+ */
+ protected function tagHook_($tag)
+ {
+ array_push($this->tags_, $tag);
+ $xml = $this->registry_->argument($tag);
+
+ // Add possible dependancies
+ if (isset($xml))
+ $this->addDependency_('tag', $tag, $xml->requires);
+ }
+
+ protected function validType_($token)
+ {
+ foreach ($this->arguments_ as $arg)
+ {
+ if ($arg['occurrence'] == '0')
+ {
+ array_shift($this->arguments_);
+ continue;
+ }
+
+ if ($token->is($arg['type']))
+ return;
+
+ // Is the argument required
+ if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*')
+ throw new SieveException($token, $arg['type']);
+
+ array_shift($this->arguments_);
+ }
+
+ // Check if command expects any (more) arguments
+ if (empty($this->arguments_))
+ throw new SieveException($token, $this->followupToken_);
+
+ throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
+ }
+
+ public function startStringList($token)
+ {
+ $this->validType_($token);
+ $this->arguments_[0]['type'] = SieveToken::String;
+ $this->arguments_[0]['occurrence'] = '+';
+ }
+
+ public function continueStringList()
+ {
+ $this->arguments_[0]['occurrence'] = '+';
+ }
+
+ public function endStringList()
+ {
+ array_shift($this->arguments_);
+ }
+
+ public function validateToken($token)
+ {
+ // Make sure the argument has a valid type
+ $this->validType_($token);
+
+ foreach ($this->arguments_ as &$arg)
+ {
+ // Build regular expression according to argument type
+ switch ($arg['type'])
+ {
+ case SieveToken::String:
+ case SieveToken::StringList:
+ $regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/'
+ . ($arg['case'] == 'ignore' ? 'si' : 's');
+ break;
+ case SieveToken::Tag:
+ $regex = '/^:(?P<one>'. $arg['regex'] .')$/si';
+ break;
+ default:
+ $regex = '/^(?P<one>'. $arg['regex'] .')$/si';
+ }
+
+ if (preg_match($regex, $token->text, $match))
+ {
+ $text = ($match['one'] ? $match['one'] : $match['two']);
+
+ // Add argument(s) that may now appear after this one
+ if (isset($arg['subArgs']))
+ $this->addArguments_($text, $arg['subArgs']);
+
+ // Call extra processing function if defined
+ if (isset($arg['call']))
+ $this->invoke_($token, $arg['call'], $text);
+
+ // Check if a possible value of this argument may occur
+ if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1')
+ {
+ $arg['occurrence'] = '0';
+ }
+ else if ($arg['occurrence'] == '+')
+ {
+ $arg['occurrence'] = '*';
+ }
+
+ return;
+ }
+
+ if ($token->is($arg['type']) && $arg['occurrence'] == 1)
+ {
+ throw new SieveException($token,
+ SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected');
+ }
+ }
+
+ throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
+ }
+
+ public function done($token)
+ {
+ // Check if there are required arguments left
+ foreach ($this->arguments_ as $arg)
+ {
+ if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1')
+ throw new SieveException($token, $arg['type']);
+ }
+
+ // Check if the command depends on use of a certain tag
+ foreach ($this->deps_ as $d)
+ {
+ switch ($d['type'])
+ {
+ case 'addresspart':
+ $values = array($this->addressPart_);
+ break;
+
+ case 'matchtype':
+ $values = array($this->matchType_);
+ break;
+
+ case 'comparator':
+ $values = array($this->comparator_);
+ break;
+
+ case 'tag':
+ $values = $this->tags_;
+ break;
+ }
+
+ foreach ($values as $value)
+ {
+ if (preg_match('/^'. $d['regex'] .'$/mi', $value))
+ break 2;
+ }
+
+ throw new SieveException($token,
+ $d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']);
+ }
+ }
+}
diff --git a/data/web/inc/lib/sieve/SieveToken.php b/data/web/inc/lib/sieve/SieveToken.php
new file mode 100644
index 00000000..459f45bc
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveToken.php
@@ -0,0 +1,88 @@
+<?php namespace Sieve;
+
+include_once('SieveDumpable.php');
+
+class SieveToken implements SieveDumpable
+{
+ const Unknown = 0x0000;
+ const ScriptEnd = 0x0001;
+ const LeftBracket = 0x0002;
+ const RightBracket = 0x0004;
+ const BlockStart = 0x0008;
+ const BlockEnd = 0x0010;
+ const LeftParenthesis = 0x0020;
+ const RightParenthesis = 0x0040;
+ const Comma = 0x0080;
+ const Semicolon = 0x0100;
+ const Whitespace = 0x0200;
+ const Tag = 0x0400;
+ const QuotedString = 0x0800;
+ const Number = 0x1000;
+ const Comment = 0x2000;
+ const MultilineString = 0x4000;
+ const Identifier = 0x8000;
+
+ const String = 0x4800; // Quoted | Multiline
+ const StringList = 0x4802; // Quoted | Multiline | LeftBracket
+ const StringListSep = 0x0084; // Comma | RightBracket
+ const Unparsed = 0x2200; // Comment | Whitespace
+ const TestList = 0x8020; // Identifier | LeftParenthesis
+
+ public $type;
+ public $text;
+ public $line;
+
+ public function __construct($type, $text, $line)
+ {
+ $this->text = $text;
+ $this->type = $type;
+ $this->line = intval($line);
+ }
+
+ public function dump()
+ {
+ return '<'. SieveToken::escape($this->text) .'> type:'. SieveToken::typeString($this->type) .' line:'. $this->line;
+ }
+
+ public function text()
+ {
+ return $this->text;
+ }
+
+ public function is($type)
+ {
+ return (bool)($this->type & $type);
+ }
+
+ public static function typeString($type)
+ {
+ switch ($type)
+ {
+ case SieveToken::Identifier: return 'identifier';
+ case SieveToken::Whitespace: return 'whitespace';
+ case SieveToken::QuotedString: return 'quoted string';
+ case SieveToken::Tag: return 'tag';
+ case SieveToken::Semicolon: return 'semicolon';
+ case SieveToken::LeftBracket: return 'left bracket';
+ case SieveToken::RightBracket: return 'right bracket';
+ case SieveToken::BlockStart: return 'block start';
+ case SieveToken::BlockEnd: return 'block end';
+ case SieveToken::LeftParenthesis: return 'left parenthesis';
+ case SieveToken::RightParenthesis: return 'right parenthesis';
+ case SieveToken::Comma: return 'comma';
+ case SieveToken::Number: return 'number';
+ case SieveToken::Comment: return 'comment';
+ case SieveToken::MultilineString: return 'multiline string';
+ case SieveToken::ScriptEnd: return 'script end';
+ case SieveToken::String: return 'string';
+ case SieveToken::StringList: return 'string list';
+ default: return 'unknown token';
+ }
+ }
+
+ protected static $tr_ = array("\r" => '\r', "\n" => '\n', "\t" => '\t');
+ public static function escape($val)
+ {
+ return strtr($val, self::$tr_);
+ }
+}
diff --git a/data/web/inc/lib/sieve/SieveTree.php b/data/web/inc/lib/sieve/SieveTree.php
new file mode 100644
index 00000000..49c73496
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveTree.php
@@ -0,0 +1,117 @@
+<?php namespace Sieve;
+
+class SieveTree
+{
+ protected $childs_;
+ protected $parents_;
+ protected $nodes_;
+ protected $max_id_;
+ protected $dump_;
+
+ public function __construct($name = 'tree')
+ {
+ $this->childs_ = array();
+ $this->parents_ = array();
+ $this->nodes_ = array();
+ $this->max_id_ = 0;
+
+ $this->parents_[0] = null;
+ $this->nodes_[0] = $name;
+ }
+
+ public function addChild(SieveDumpable $child)
+ {
+ return $this->addChildTo($this->max_id_, $child);
+ }
+
+ public function addChildTo($parent_id, SieveDumpable $child)
+ {
+ if (!is_int($parent_id)
+ || !isset($this->nodes_[$parent_id]))
+ return null;
+
+ if (!isset($this->childs_[$parent_id]))
+ $this->childs_[$parent_id] = array();
+
+ $child_id = ++$this->max_id_;
+ $this->nodes_[$child_id] = $child;
+ $this->parents_[$child_id] = $parent_id;
+ array_push($this->childs_[$parent_id], $child_id);
+
+ return $child_id;
+ }
+
+ public function getRoot()
+ {
+ return 0;
+ }
+
+ public function getChilds($node_id)
+ {
+ if (!is_int($node_id)
+ || !isset($this->nodes_[$node_id]))
+ return null;
+
+ if (!isset($this->childs_[$node_id]))
+ return array();
+
+ return $this->childs_[$node_id];
+ }
+
+ public function getNode($node_id)
+ {
+ if ($node_id == 0 || !is_int($node_id)
+ || !isset($this->nodes_[$node_id]))
+ return null;
+
+ return $this->nodes_[$node_id];
+ }
+
+ public function dump()
+ {
+ $this->dump_ = $this->nodes_[$this->getRoot()] ."\n";
+ $this->dumpChilds_($this->getRoot(), ' ');
+ return $this->dump_;
+ }
+
+ protected function dumpChilds_($parent_id, $prefix)
+ {
+ if (!isset($this->childs_[$parent_id]))
+ return;
+
+ $childs = $this->childs_[$parent_id];
+ $last_child = count($childs);
+
+ for ($i=1; $i <= $last_child; ++$i)
+ {
+ $child_node = $this->nodes_[$childs[$i-1]];
+ $infix = ($i == $last_child ? '`--- ' : '|--- ');
+ $this->dump_ .= $prefix . $infix . $child_node->dump() . " (id:" . $childs[$i-1] . ")\n";
+
+ $next_prefix = $prefix . ($i == $last_child ? ' ' : '| ');
+ $this->dumpChilds_($childs[$i-1], $next_prefix);
+ }
+ }
+
+ public function getText()
+ {
+ $this->dump_ = '';
+ $this->childText_($this->getRoot());
+ return $this->dump_;
+ }
+
+ protected function childText_($parent_id)
+ {
+ if (!isset($this->childs_[$parent_id]))
+ return;
+
+ $childs = $this->childs_[$parent_id];
+
+ for ($i = 0; $i < count($childs); ++$i)
+ {
+ $child_node = $this->nodes_[$childs[$i]];
+ $this->dump_ .= $child_node->text();
+ $this->childText_($childs[$i]);
+ }
+ }
+}
diff --git a/data/web/inc/lib/sieve/extensions/body.xml b/data/web/inc/lib/sieve/extensions/body.xml
new file mode 100644
index 00000000..657b845e
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/body.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="body">
+
+ <test name="body">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="tag" name="body transform" regex="(raw|content|text)" occurrence="optional">
+ <parameter type="stringlist" name="content types" follows="content" />
+ </parameter>
+ <parameter type="stringlist" name="key list" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml b/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
new file mode 100644
index 00000000..6f96f8d1
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="comparator-i;ascii-numeric">
+
+ <comparator name="i;ascii-numeric" />
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/copy.xml b/data/web/inc/lib/sieve/extensions/copy.xml
new file mode 100644
index 00000000..4e3f9020
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/copy.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="copy">
+
+ <tagged-argument extends="(fileinto|redirect)">
+ <parameter type="tag" name="copy" regex="copy" occurrence="optional" />
+ </tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/date.xml b/data/web/inc/lib/sieve/extensions/date.xml
new file mode 100644
index 00000000..08f65409
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/date.xml
@@ -0,0 +1,28 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="date">
+
+ <test name="date">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="tag" name="zone" regex="(zone|originalzone)" occurrence="optional">
+ <parameter type="string" name="time-zone" follows="zone" />
+ </parameter>
+ <parameter type="string" name="header-name" />
+ <parameter type="string" case="ignore" name="date-part"
+ regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
+ <parameter type="stringlist" name="key-list" />
+ </test>
+
+ <test name="currentdate">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="tag" name="zone" regex="zone" occurrence="optional">
+ <parameter type="string" name="time-zone" />
+ </parameter>
+ <parameter type="string" case="ignore" name="date-part"
+ regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
+ <parameter type="stringlist" name="key-list" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/editheader.xml b/data/web/inc/lib/sieve/extensions/editheader.xml
new file mode 100644
index 00000000..52244820
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/editheader.xml
@@ -0,0 +1,22 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="editheader">
+
+ <command name="addheader">
+ <parameter type="tag" name="last" regex="last" occurrence="optional" />
+ <parameter type="string" name="field name" />
+ <parameter type="string" name="value" />
+ </command>
+
+ <command name="deleteheader">
+ <parameter type="tag" name="index" regex="index" occurrence="optional">
+ <parameter type="number" name="field number" />
+ <parameter type="tag" name="last" regex="last" occurrence="optional" />
+ </parameter>
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="string" name="field name" />
+ <parameter type="stringlist" name="value patterns" occurrence="optional" />
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/envelope.xml b/data/web/inc/lib/sieve/extensions/envelope.xml
new file mode 100644
index 00000000..ce88ada9
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/envelope.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="envelope">
+
+ <test name="envelope">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="addresspart" occurrence="optional" />
+ <parameter type="stringlist" name="envelope-part" />
+ <parameter type="stringlist" name="key" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/environment.xml b/data/web/inc/lib/sieve/extensions/environment.xml
new file mode 100644
index 00000000..edaab8df
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/environment.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="environment">
+
+ <test name="environment">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="string" name="name"
+ regex="(domain|host|location|name|phase|remote-host|remote-ip|version|vnd\..+)" />
+ <parameter type="stringlist" name="key-list" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/ereject.xml b/data/web/inc/lib/sieve/extensions/ereject.xml
new file mode 100644
index 00000000..f7230195
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/ereject.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="ereject">
+
+ <command name="ereject">
+
+ <parameter type="string" name="reason" />
+
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/fileinto.xml b/data/web/inc/lib/sieve/extensions/fileinto.xml
new file mode 100644
index 00000000..3b48a5c0
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/fileinto.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="fileinto">
+
+ <command name="fileinto">
+ <parameter type="string" name="folder" />
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/imap4flags.xml b/data/web/inc/lib/sieve/extensions/imap4flags.xml
new file mode 100644
index 00000000..5f6d176e
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/imap4flags.xml
@@ -0,0 +1,29 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="imap4flags">
+
+ <command name="setflag">
+ <parameter type="stringlist" name="flag list" />
+ </command>
+
+ <command name="addflag">
+ <parameter type="stringlist" name="flag list" />
+ </command>
+
+ <command name="removeflag">
+ <parameter type="stringlist" name="flag list" />
+ </command>
+
+ <test name="hasflag">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="stringlist" name="flag list" />
+ </test>
+
+ <tagged-argument extends="(fileinto|keep)">
+ <parameter type="tag" name="flags" regex="flags" occurrence="optional">
+ <parameter type="stringlist" name="flag list" />
+ </parameter>
+ </tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/imapflags.xml b/data/web/inc/lib/sieve/extensions/imapflags.xml
new file mode 100644
index 00000000..4b78cc80
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/imapflags.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="imapflags">
+
+ <command name="mark" />
+
+ <command name="unmark" />
+
+ <command name="setflag">
+ <parameter type="stringlist" name="flag list" />
+ </command>
+
+ <command name="addflag">
+ <parameter type="stringlist" name="flag list" />
+ </command>
+
+ <command name="removeflag">
+ <parameter type="stringlist" name="flag list" />
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/index.xml b/data/web/inc/lib/sieve/extensions/index.xml
new file mode 100644
index 00000000..f81055c5
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/index.xml
@@ -0,0 +1,17 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="index">
+
+ <tagged-argument extends="(header|address|date)">
+ <parameter type="tag" name="index" regex="index" occurrence="optional">
+ <parameter type="number" name="field number" />
+ </parameter>
+ </tagged-argument>
+
+ <tagged-argument extends="(header|address|date)">
+ <parameter type="tag" name="last" regex="last" occurrence="optional">
+ <requires type="tag" name="index" regex="index" />
+ </parameter>
+ </tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/notify.xml b/data/web/inc/lib/sieve/extensions/notify.xml
new file mode 100644
index 00000000..e1702e95
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/notify.xml
@@ -0,0 +1,29 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="notify">
+
+ <command name="notify">
+ <parameter type="tag" name="method" regex="method" occurrence="optional">
+ <parameter type="string" name="method-name" />
+ </parameter>
+
+ <parameter type="tag" name="id" regex="id" occurrence="optional">
+ <parameter type="string" name="message-id" />
+ </parameter>
+
+ <parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
+
+ <parameter type="tag" name="message" regex="message" occurrence="optional">
+ <parameter type="string" name="message-text" />
+ </parameter>
+ </command>
+
+ <command name="denotify">
+ <parameter type="matchtype" occurrence="optional">
+ <parameter type="string" name="message-id" />
+ </parameter>
+
+ <parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/regex.xml b/data/web/inc/lib/sieve/extensions/regex.xml
new file mode 100644
index 00000000..79d67fc0
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/regex.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="regex">
+
+ <matchtype name="regex" />
+
+ <tagged-argument extends="set">
+ <parameter type="tag" name="modifier" regex="quoteregex" occurrence="optional" />
+ </tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/reject.xml b/data/web/inc/lib/sieve/extensions/reject.xml
new file mode 100644
index 00000000..33d2573f
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/reject.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="reject">
+
+ <command name="reject">
+
+ <parameter type="string" name="reason" />
+
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/relational.xml b/data/web/inc/lib/sieve/extensions/relational.xml
new file mode 100644
index 00000000..b9e2b39e
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/relational.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="relational">
+
+ <matchtype name="count">
+ <requires type="comparator" name="i;ascii-numeric" regex="i;ascii-numeric" />
+ <parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
+ </matchtype>
+
+ <matchtype name="value">
+ <parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
+ </matchtype>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/spamtest.xml b/data/web/inc/lib/sieve/extensions/spamtest.xml
new file mode 100644
index 00000000..06c8c3bc
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/spamtest.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="spamtest">
+
+ <test name="spamtest">
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="string" name="value" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/spamtestplus.xml b/data/web/inc/lib/sieve/extensions/spamtestplus.xml
new file mode 100644
index 00000000..c7b768d9
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/spamtestplus.xml
@@ -0,0 +1,12 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="spamtestplus">
+
+ <test name="spamtest" overrides="true">
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="tag" name="percent" regex="percent" occurrence="optional" />
+ <parameter type="string" name="value" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/subaddress.xml b/data/web/inc/lib/sieve/extensions/subaddress.xml
new file mode 100644
index 00000000..a668fdf9
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/subaddress.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="subaddress">
+
+ <addresspart name="user" />
+ <addresspart name="detail" />
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/vacation.xml b/data/web/inc/lib/sieve/extensions/vacation.xml
new file mode 100644
index 00000000..dbd6992a
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/vacation.xml
@@ -0,0 +1,31 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="vacation">
+
+ <command name="vacation">
+ <parameter type="tag" name="days" occurrence="optional" regex="days">
+ <parameter type="number" name="period" />
+ </parameter>
+
+ <parameter type="tag" name="addresses" occurrence="optional" regex="addresses">
+ <parameter type="stringlist" name="address strings" />
+ </parameter>
+
+ <parameter type="tag" name="subject" occurrence="optional" regex="subject">
+ <parameter type="string" name="subject string" />
+ </parameter>
+
+ <parameter type="tag" name="from" occurrence="optional" regex="from">
+ <parameter type="string" name="from string" />
+ </parameter>
+
+ <parameter type="tag" name="handle" occurrence="optional" regex="handle">
+ <parameter type="string" name="handle string" />
+ </parameter>
+
+ <parameter type="tag" name="mime" occurrence="optional" regex="mime" />
+
+ <parameter type="string" name="reason" />
+ </command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/variables.xml b/data/web/inc/lib/sieve/extensions/variables.xml
new file mode 100644
index 00000000..d9ff0008
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/variables.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="variables">
+
+ <command name="set">
+ <parameter type="tag" name="modifier" regex="(lower|upper)" occurrence="optional" />
+ <parameter type="tag" name="modifier" regex="(lower|upper)first" occurrence="optional" />
+ <parameter type="tag" name="modifier" regex="quotewildcard" occurrence="optional" />
+ <parameter type="tag" name="modifier" regex="length" occurrence="optional" />
+ <parameter type="string" name="name" regex="[[:alpha:]_][[:alnum:]_]*" />
+ <parameter type="string" name="value" />
+ </command>
+
+ <test name="string">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="stringlist" name="source" />
+ <parameter type="stringlist" name="key list" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/virustest.xml b/data/web/inc/lib/sieve/extensions/virustest.xml
new file mode 100644
index 00000000..6dac8e87
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/virustest.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="virustest">
+
+ <test name="virustest">
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="string" name="value" />
+ </test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/keywords.xml b/data/web/inc/lib/sieve/keywords.xml
new file mode 100644
index 00000000..1ab7c4d2
--- /dev/null
+++ b/data/web/inc/lib/sieve/keywords.xml
@@ -0,0 +1,91 @@
+<?xml version='1.0' standalone='yes'?>
+
+<keywords>
+
+ <matchtype name="is" />
+ <matchtype name="contains" />
+ <matchtype name="matches" />
+ <matchtype name="value">
+ <parameter type="string" name="operator" regex="(gt|ge|eq|le|lt)" />
+ </matchtype>
+
+
+ <comparator name="i;octet" />
+ <comparator name="i;ascii-casemap" />
+ <comparator name="i;unicode-casemap" />
+
+ <addresspart name="all" />
+ <addresspart name="localpart" />
+ <addresspart name="domain" />
+
+
+ <command name="discard" />
+
+ <command name="elsif">
+ <parameter type="test" name="test command" />
+ <parameter type="block" />
+ </command>
+
+ <command name="else">
+ <parameter type="block" />
+ </command>
+
+ <command name="if">
+ <parameter type="test" name="test command" />
+ <parameter type="block" />
+ </command>
+
+ <command name="keep" />
+
+ <command name="redirect">
+ <parameter type="string" name="address string" />
+ </command>
+
+ <command name="require">
+ <parameter type="requirestrings" name="require string" />
+ </command>
+
+ <command name="stop" />
+
+
+ <test name="address">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="addresspart" occurrence="optional" />
+ <parameter type="stringlist" name="header list" />
+ <parameter type="stringlist" name="key list" />
+ </test>
+
+ <test name="allof">
+ <parameter type="testlist" name="test" />
+ </test>
+
+ <test name="anyof">
+ <parameter type="testlist" name="test" />
+ </test>
+
+ <test name="exists">
+ <parameter type="stringlist" name="header names" />
+ </test>
+
+ <test name="false" />
+
+ <test name="header">
+ <parameter type="matchtype" occurrence="optional" />
+ <parameter type="comparator" occurrence="optional" />
+ <parameter type="stringlist" name="header names" />
+ <parameter type="stringlist" name="key list" />
+ </test>
+
+ <test name="not">
+ <parameter type="test" />
+ </test>
+
+ <test name="size">
+ <parameter type="tag" regex="(over|under)" />
+ <parameter type="number" name="limit" />
+ </test>
+
+ <test name="true" />
+
+</keywords>
diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php
index cdb88804..1533e24f 100644
--- a/data/web/inc/prerequisites.inc.php
+++ b/data/web/inc/prerequisites.inc.php
@@ -16,6 +16,9 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/Yubico.php';
// Autoload composer
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
+// Load Sieve
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/sieve/SieveParser.php';
+
// U2F API + T/HOTP API
$u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']);
$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL);
@@ -70,6 +73,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
init_db_schema();
diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php
index abbef40f..fbdabfdc 100644
--- a/data/web/inc/sessions.inc.php
+++ b/data/web/inc/sessions.inc.php
@@ -62,6 +62,8 @@ if (isset($_POST["logout"])) {
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
unset($_SESSION["dual-login"]);
+ header("Location: /mailbox.php");
+ exit();
}
else {
session_regenerate_id(true);
diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php
index 98ceee7c..2b93af15 100644
--- a/data/web/inc/vars.inc.php
+++ b/data/web/inc/vars.inc.php
@@ -104,6 +104,9 @@ $MAILCOW_APPS = array(
// Rows until pagination begins
$PAGINATION_SIZE = 10;
+// Default number of rows/lines to display (log table)
+$LOG_LINES = 100;
+
// Rows until pagination begins (log table)
$LOG_PAGINATION_SIZE = 30;
diff --git a/data/web/js/admin.js b/data/web/js/admin.js
index 83829ab7..c368cbe7 100644
--- a/data/web/js/admin.js
+++ b/data/web/js/admin.js
@@ -1,125 +1,10 @@
-var Base64 = {
- _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
- encode: function(e) {
- var t = "";
- var n, r, i, s, o, u, a;
- var f = 0;
- e = Base64._utf8_encode(e);
- while (f < e.length) {
- n = e.charCodeAt(f++);
- r = e.charCodeAt(f++);
- i = e.charCodeAt(f++);
- s = n >> 2;
- o = (n & 3) << 4 | r >> 4;
- u = (r & 15) << 2 | i >> 6;
- a = i & 63;
- if (isNaN(r)) {
- u = a = 64
- } else if (isNaN(i)) {
- a = 64
- }
- t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) +
- this._keyStr.charAt(u) + this._keyStr.charAt(a)
- }
- return t
- },
- decode: function(e) {
- var t = "";
- var n, r, i;
- var s, o, u, a;
- var f = 0;
- e = e.replace(/[^A-Za-z0-9\+\/\=]/g, "");
- while (f < e.length) {
- s = this._keyStr.indexOf(e.charAt(f++));
- o = this._keyStr.indexOf(e.charAt(f++));
- u = this._keyStr.indexOf(e.charAt(f++));
- a = this._keyStr.indexOf(e.charAt(f++));
- n = s << 2 | o >> 4;
- r = (o & 15) << 4 | u >> 2;
- i = (u & 3) << 6 | a;
- t = t + String.fromCharCode(n);
- if (u != 64) {
- t = t + String.fromCharCode(r)
- }
- if (a != 64) {
- t = t + String.fromCharCode(i)
- }
- }
- t = Base64._utf8_decode(t);
- return t
- },
- _utf8_encode: function(e) {
- e = e.replace(/\r\n/g, "\n");
- var t = "";
- for (var n = 0; n < e.length; n++) {
- var r = e.charCodeAt(n);
- if (r < 128) {
- t += String.fromCharCode(r)
- } else if (r > 127 && r < 2048) {
- t += String.fromCharCode(r >> 6 | 192);
- t += String.fromCharCode(r & 63 | 128)
- } else {
- t += String.fromCharCode(r >> 12 | 224);
- t += String.fromCharCode(r >> 6 & 63 | 128);
- t += String.fromCharCode(r & 63 | 128)
- }
- }
- return t
- },
- _utf8_decode: function(e) {
- var t = "";
- var n = 0;
- var r = c1 = c2 = 0;
- while (n < e.length) {
- r = e.charCodeAt(n);
- if (r < 128) {
- t += String.fromCharCode(r);
- n++
- } else if (r > 191 && r < 224) {
- c2 = e.charCodeAt(n + 1);
- t += String.fromCharCode((r & 31) << 6 | c2 & 63);
- n += 2
- } else {
- c2 = e.charCodeAt(n + 1);
- c3 = e.charCodeAt(n + 2);
- t += String.fromCharCode((r & 15) << 12 | (c2 & 63) <<
- 6 | c3 & 63);
- n += 3
- }
- }
- return t
- }
-}
-
+// Base64 functions
+var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
jQuery(function($){
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
- var entityMap = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- '/': '/',
- '`': '`',
- '=': '='
- };
- function escapeHtml(string) {
- return String(string).replace(/[&<>"'`=\/]/g, function (s) {
- return entityMap[s];
- });
- }
- function humanFileSize(bytes) {
- if(Math.abs(bytes) < 1024) {
- return bytes + ' B';
- }
- var units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
- var u = -1;
- do {
- bytes /= 1024;
- ++u;
- } while(Math.abs(bytes) >= 1024 && u < units.length - 1);
- return bytes.toFixed(1)+' '+units[u];
- }
+ var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};
+ 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]}
$("#refresh_postfix_log").on('click', function(e) {
e.preventDefault();
draw_postfix_logs();
@@ -148,97 +33,69 @@ jQuery(function($){
e.preventDefault();
$('#import_dkim_arrow').toggleClass("animation");
});
- function draw_postfix_logs() {
- ft_postfix_logs = FooTable.init('#postfix_log', {
+ function draw_autodiscover_logs() {
+ ft_autodiscover_logs = FooTable.init('#autodiscover_log', {
"columns": [
{"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
- {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
- {"name":"message","title":lang.message},
+ {"name":"ua","title":"User-Agent","style":{"min-width":"200px"}},
+ {"name":"user","title":"Username","style":{"min-width":"200px"}},
+ {"name":"service","title":"Service"},
],
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/logs/postfix/1000',
+ url: '/api/v1/get/logs/autodiscover/100',
jsonp: false,
error: function () {
- console.log('Cannot draw postfix log table');
+ console.log('Cannot draw autodiscover log table');
},
success: function (data) {
- $.each(data, function (i, item) {
- item.message = escapeHtml(item.message);
- var danger_class = ["emerg", "alert", "crit", "err"];
- var warning_class = ["warning", "warn"];
- var info_class = ["notice", "info", "debug"];
- if (jQuery.inArray(item.priority, danger_class) !== -1) {
- item.priority = '<span class="label label-danger">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, warning_class) !== -1) {
- item.priority = '<span class="label label-warning">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, info_class) !== -1) {
- item.priority = '<span class="label label-info">' + item.priority + '</span>';
- }
- });
+ return process_table_data(data, 'autodiscover_log');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "placeholder": lang.filter_table
- },
- "sorting": {
- "enabled": true
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
+ "sorting": {"enabled": true},
+ "on": {"ready.ft.table": function(e, ft){
+ heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ $(heading).children('.log-lines').text(function(){
+ var ft_paging = ft.use(FooTable.Paging)
+ return ft_paging.totalRows;
+ })
+ }
}
});
}
- function draw_autodiscover_logs() {
- ft_autodiscover_logs = FooTable.init('#autodiscover_log', {
+ function draw_postfix_logs() {
+ ft_postfix_logs = FooTable.init('#postfix_log', {
"columns": [
{"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
- {"name":"ua","title":"User-Agent","style":{"min-width":"200px"}},
- {"name":"user","title":"Username","style":{"min-width":"200px"}},
- {"name":"service","title":"Service"},
+ {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
+ {"name":"message","title":lang.message},
],
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/logs/autodiscover/100',
+ url: '/api/v1/get/logs/postfix',
jsonp: false,
error: function () {
- console.log('Cannot draw autodiscover log table');
+ console.log('Cannot draw postfix log table');
},
success: function (data) {
- $.each(data, function (i, item) {
- item.ua = '<span style="font-size:small">' + item.ua + '</span>';
- if (item.service == "activesync") {
- item.service = '<span class="label label-info">ActiveSync</span>';
- }
- else if (item.service == "imap") {
- item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>';
- }
- else {
- item.service = '<span class="label label-danger">' + item.service + '</span>';
- }
- });
+ return process_table_data(data, 'general_syslog');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "placeholder": lang.filter_table
- },
- "sorting": {
- "enabled": true
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
+ "sorting": {"enabled": true},
+ "on": {
+ "ready.ft.table": function(e, ft){
+ heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ $(heading).children('.log-lines').text(function(){
+ var ft_paging = ft.use(FooTable.Paging)
+ return ft_paging.totalRows;
+ })
+ }
}
});
}
@@ -251,43 +108,27 @@ jQuery(function($){
],
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/logs/fail2ban/1000',
+ url: '/api/v1/get/logs/fail2ban',
jsonp: false,
error: function () {
console.log('Cannot draw fail2ban log table');
},
success: function (data) {
- $.each(data, function (i, item) {
- var danger_class = ["emerg", "alert", "crit", "err"];
- var warning_class = ["warning", "warn"];
- var info_class = ["notice", "info", "debug"];
- item.message = escapeHtml(item.message);
- if (jQuery.inArray(item.priority, danger_class) !== -1) {
- item.priority = '<span class="label label-danger">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, warning_class) !== -1) {
- item.priority = '<span class="label label-warning">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, info_class) !== -1) {
- item.priority = '<span class="label label-info">' + item.priority + '</span>';
- }
- });
+ return process_table_data(data, 'general_syslog');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "connectors": false,
- "placeholder": lang.filter_table
- },
- "sorting": {
- "enabled": true
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+ "sorting": {"enabled": true},
+ "on": {
+ "ready.ft.table": function(e, ft){
+ heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ $(heading).children('.log-lines').text(function(){
+ var ft_paging = ft.use(FooTable.Paging)
+ return ft_paging.totalRows;
+ })
+ }
}
});
}
@@ -300,48 +141,32 @@ jQuery(function($){
],
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/logs/sogo/1000',
+ url: '/api/v1/get/logs/sogo',
jsonp: false,
error: function () {
console.log('Cannot draw sogo log table');
},
success: function (data) {
- $.each(data, function (i, item) {
- var danger_class = ["emerg", "alert", "crit", "err"];
- var warning_class = ["warning", "warn"];
- var info_class = ["notice", "info", "debug"];
- item.message = escapeHtml(item.message);
- if (jQuery.inArray(item.priority, danger_class) !== -1) {
- item.priority = '<span class="label label-danger">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, warning_class) !== -1) {
- item.priority = '<span class="label label-warning">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, info_class) !== -1) {
- item.priority = '<span class="label label-info">' + item.priority + '</span>';
- }
- });
+ return process_table_data(data, 'general_syslog');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "connectors": false,
- "placeholder": lang.filter_table
- },
- "sorting": {
- "enabled": true
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+ "sorting": {"enabled": true},
+ "on": {
+ "ready.ft.table": function(e, ft){
+ heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ $(heading).children('.log-lines').text(function(){
+ var ft_paging = ft.use(FooTable.Paging)
+ return ft_paging.totalRows;
+ })
+ }
}
});
}
function draw_dovecot_logs() {
- ft_postfix_logs = FooTable.init('#dovecot_log', {
+ ft_dovecot_logs = FooTable.init('#dovecot_log', {
"columns": [
{"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
{"name":"priority","title":lang.priority,"style":{"width":"80px"}},
@@ -349,43 +174,27 @@ jQuery(function($){
],
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/logs/dovecot/1000',
+ url: '/api/v1/get/logs/dovecot',
jsonp: false,
error: function () {
console.log('Cannot draw dovecot log table');
},
success: function (data) {
- $.each(data, function (i, item) {
- var danger_class = ["emerg", "alert", "crit", "err"];
- var warning_class = ["warning", "warn"];
- var info_class = ["notice", "info", "debug"];
- item.message = escapeHtml(item.message);
- if (jQuery.inArray(item.priority, danger_class) !== -1) {
- item.priority = '<span class="label label-danger">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, warning_class) !== -1) {
- item.priority = '<span class="label label-warning">' + item.priority + '</span>';
- }
- else if (jQuery.inArray(item.priority, info_class) !== -1) {
- item.priority = '<span class="label label-info">' + item.priority + '</span>';
- }
- });
+ return process_table_data(data, 'general_syslog');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "connectors": false,
- "placeholder": lang.filter_table
- },
- "sorting": {
- "enabled": true
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+ "sorting": {"enabled": true},
+ "on": {
+ "ready.ft.table": function(e, ft){
+ heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ $(heading).children('.log-lines').text(function(){
+ var ft_paging = ft.use(FooTable.Paging)
+ return ft_paging.totalRows;
+ })
+ }
}
});
}
@@ -407,30 +216,14 @@ jQuery(function($){
console.log('Cannot draw domain admin table');
},
success: function (data) {
- $.each(data, function (i, item) {
- item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
- item.action = '<div class="btn-group">' +
- '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
- '<a href="#" id="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
- '</div>';
- });
+ return process_table_data(data, 'domainadminstable');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "connectors": false,
- "placeholder": lang.filter_table
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table
},
- "sorting": {
- "enabled": true
- }
+ "sorting": {"enabled": true}
});
}
function draw_fwd_hosts() {
@@ -450,33 +243,16 @@ jQuery(function($){
console.log('Cannot draw forwarding hosts table');
},
success: function (data) {
- $.each(data, function (i, item) {
- item.action = '<div class="btn-group">' +
- '<a href="#" id="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
- '</div>';
- if (item.keep_spam == "yes") {
- item.keep_spam = lang.no;
- }
- else {
- item.keep_spam = lang.yes;
- }
- item.chkbox = '<input type="checkbox" data-id="fwdhosts" name="multi_select" value="' + item.host + '" />';
- });
+ return process_table_data(data, 'forwardinghoststable');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "sorting": {
- "enabled": true
- }
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "sorting": {"enabled": true}
});
}
function draw_relayhosts() {
- ft_forwardinghoststable = FooTable.init('#relayhoststable', {
+ ft_relayhoststable = FooTable.init('#relayhoststable', {
"columns": [
{"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
{"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
@@ -494,115 +270,30 @@ jQuery(function($){
console.log('Cannot draw forwarding hosts table');
},
success: function (data) {
- $.each(data, function (i, item) {
- item.action = '<div class="btn-group">' +
- '<a href="#" data-toggle="modal" id="miau" data-target="#testRelayhostModal" data-relayhost-id="' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-stats"></span> Test</a>' +
- '<a href="/edit.php?relayhost=' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
- '<a href="#" id="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
- '</div>';
- item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
- });
+ return process_table_data(data, 'relayhoststable');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "sorting": {
- "enabled": true
- }
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "sorting": {"enabled": true}
});
}
function draw_rspamd_history() {
- ft_postfix_logs = FooTable.init('#rspamd_history', {
- "columns": [{
- "name":"unix_time",
- "formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},
- "title":lang.time,
- "style":{
- "width":"170px"
- }
- }, {
- "name": "ip",
- "title": "IP address",
- "breakpoints": "all",
- "style": {
- "minWidth": 88
- }
- }, {
- "name": "sender_mime",
- "title": "From",
- "breakpoints": "xs sm md",
- "style": {
- "minWidth": 100
- }
- }, {
- "name": "rcpt_mime",
- "title": "To",
- "breakpoints": "xs sm md",
- "style": {
- "minWidth": 100
- }
- }, {
- "name": "subject",
- "title": "Subject",
- "breakpoints": "all",
- "style": {
- "word-break": "break-all",
- "minWidth": 150
- }
- }, {
- "name": "action",
- "title": "Action",
- "style": {
- "minwidth": 82
- }
- }, {
- "name": "score",
- "title": "Score",
- "style": {
- "maxWidth": 110
- },
- }, {
- "name": "symbols",
- "title": "Symbols",
- "breakpoints": "all",
- }, {
- "name": "size",
- "title": "Msg size",
- "breakpoints": "all",
- "style": {
- "minwidth": 50,
- },
- "formatter": function(value) { return humanFileSize(value); }
- }, {
- "name": "scan_time",
- "title": "Scan time",
- "breakpoints": "all",
- "style": {
- "maxWidth": 72
- },
- }, {
- "name": "message-id",
- "title": "ID",
- "breakpoints": "all",
- "style": {
- "minWidth": 130,
- "overflow": "hidden",
- "textOverflow": "ellipsis",
- "wordBreak": "break-all",
- "whiteSpace": "normal"
- }
- }, {
- "name": "user",
- "title": "Authenticated user",
- "breakpoints": "xs sm md",
- "style": {
- "minWidth": 100
- }
- }],
+ ft_rspamd_history = FooTable.init('#rspamd_history', {
+ "columns": [
+ {"name":"unix_time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+ {"name": "ip","title": "IP address","breakpoints": "all","style": {"minWidth": 88}},
+ {"name": "sender_mime","title": "From","breakpoints": "xs sm md","style": {"minWidth": 100}},
+ {"name": "rcpt_mime","title": "To","breakpoints": "xs sm md","style": {"minWidth": 100}},
+ {"name": "subject","title": "Subject","breakpoints": "all","style": {"word-break": "break-all","minWidth": 150}},
+ {"name": "action","title": "Action","style": {"minwidth": 82}},
+ {"name": "score","title": "Score","style": {"maxWidth": 110},},
+ {"name": "symbols","title": "Symbols","breakpoints": "all",},
+ {"name": "size","title": "Msg size","breakpoints": "all","style": {"minwidth": 50},"formatter": function(value){return humanFileSize(value);}},
+ {"name": "scan_time","title": "Scan time","breakpoints": "all","style": {"maxWidth": 72},},
+ {"name": "message-id","title": "ID","breakpoints": "all","style": {"minWidth": 130,"overflow": "hidden","textOverflow": "ellipsis","wordBreak": "break-all","whiteSpace": "normal"}},
+ {"name": "user","title": "Authenticated user","breakpoints": "xs sm md","style": {"minWidth": 100}}
+ ],
"rows": $.ajax({
dataType: 'json',
url: '/api/v1/get/logs/rspamd-history',
@@ -611,81 +302,146 @@ jQuery(function($){
console.log('Cannot draw rspamd history table');
},
success: function (data) {
- $.each(data, function (i, item) {
- item.rcpt_mime = item.rcpt_mime.join(",​");
- Object.keys(item.symbols).map(function(key) {
- var sym = item.symbols[key];
- if (sym.score <= 0) {
- sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
- }
- else {
- sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
- }
- var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
- if (sym.options) {
- str += ' [' + sym.options.join(",") + "]";
- }
- item.symbols[key].str = str;
- });
- item.symbols = Object.keys(item.symbols).
- map(function(key) {
- return item.symbols[key];
- }).sort(function(e1, e2) {
- return Math.abs(e1.score) < Math.abs(e2.score);
- }).map(function(e) {
- return e.str;
- }).join("<br>\n");
- var scan_time = item.time_real.toFixed(3) + ' / ' + item.time_virtual.toFixed(3);
- item.scan_time = {
- "options": {
- "sortValue": item.time_real
- },
- "value": scan_time
- };
- if (item.action === 'clean' || item.action === 'no action') {
- item.action = "<div class='label label-success'>" + item.action + "</div>";
- } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
- item.action = "<div class='label label-warning'>" + item.action + "</div>";
- } else if (item.action === 'spam' || item.action === 'reject') {
- item.action = "<div class='label label-danger'>" + item.action + "</div>";
- } else {
- item.action = "<div class='label label-info'>" + item.action + "</div>";
- }
- var score_content;
- if (item.score < item.required_score) {
- score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
- } else {
- score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
- }
- item.score = {
- "options": {
- "sortValue": item.score
- },
- "value": score_content
- };
- if (item.user == null) {
- item.user = "none";
- }
- });
+ return process_table_data(data, 'rspamd_history');
}
}),
"empty": lang.empty,
- "paging": {
- "enabled": true,
- "limit": 5,
- "size": log_pagination_size
- },
- "filtering": {
- "enabled": true,
- "position": "left",
- "connectors": false,
- "placeholder": lang.filter_table
- },
- "sorting": {
- "enabled": true
+ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+ "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+ "sorting": {"enabled": true},
+ "on": {
+ "ready.ft.table": function(e, ft){
+ heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ $(heading).children('.log-lines').text(function(){
+ var ft_paging = ft.use(FooTable.Paging)
+ return ft_paging.totalRows;
+ })
+ }
}
});
}
+
+ function process_table_data(data, table) {
+ if (table == 'rspamd_history') {
+ $.each(data, function (i, item) {
+ item.rcpt_mime = item.rcpt_mime.join(",​");
+ Object.keys(item.symbols).map(function(key) {
+ var sym = item.symbols[key];
+ if (sym.score <= 0) {
+ sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
+ }
+ else {
+ sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
+ }
+ var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
+ if (sym.options) {
+ str += ' [' + sym.options.join(",") + "]";
+ }
+ item.symbols[key].str = str;
+ });
+ item.symbols = Object.keys(item.symbols).
+ map(function(key) {
+ return item.symbols[key];
+ }).sort(function(e1, e2) {
+ return Math.abs(e1.score) < Math.abs(e2.score);
+ }).map(function(e) {
+ return e.str;
+ }).join("<br>\n");
+ var scan_time = item.time_real.toFixed(3) + ' / ' + item.time_virtual.toFixed(3);
+ item.scan_time = {
+ "options": {
+ "sortValue": item.time_real
+ },
+ "value": scan_time
+ };
+ if (item.action === 'clean' || item.action === 'no action') {
+ item.action = "<div class='label label-success'>" + item.action + "</div>";
+ } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
+ item.action = "<div class='label label-warning'>" + item.action + "</div>";
+ } else if (item.action === 'spam' || item.action === 'reject') {
+ item.action = "<div class='label label-danger'>" + item.action + "</div>";
+ } else {
+ item.action = "<div class='label label-info'>" + item.action + "</div>";
+ }
+ var score_content;
+ if (item.score < item.required_score) {
+ score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+ } else {
+ score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+ }
+ item.score = {
+ "options": {
+ "sortValue": item.score
+ },
+ "value": score_content
+ };
+ if (item.user == null) {
+ item.user = "none";
+ }
+ });
+ } else if (table == 'relayhoststable') {
+ $.each(data, function (i, item) {
+ item.action = '<div class="btn-group">' +
+ '<a href="#" data-toggle="modal" id="miau" data-target="#testRelayhostModal" data-relayhost-id="' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-stats"></span> Test</a>' +
+ '<a href="/edit.php?relayhost=' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+ '<a href="#" id="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+ '</div>';
+ item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
+ });
+ } else if (table == 'forwardinghoststable') {
+ $.each(data, function (i, item) {
+ item.action = '<div class="btn-group">' +
+ '<a href="#" id="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+ '</div>';
+ if (item.keep_spam == "yes") {
+ item.keep_spam = lang.no;
+ }
+ else {
+ item.keep_spam = lang.yes;
+ }
+ item.chkbox = '<input type="checkbox" data-id="fwdhosts" name="multi_select" value="' + item.host + '" />';
+ });
+ } else if (table == 'domainadminstable') {
+ $.each(data, function (i, item) {
+ item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
+ item.action = '<div class="btn-group">' +
+ '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+ '<a href="#" id="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+ '</div>';
+ });
+ } else if (table == 'autodiscover_log') {
+ $.each(data, function (i, item) {
+ item.ua = '<span style="font-size:small">' + item.ua + '</span>';
+ if (item.service == "activesync") {
+ item.service = '<span class="label label-info">ActiveSync</span>';
+ }
+ else if (item.service == "imap") {
+ item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>';
+ }
+ else {
+ item.service = '<span class="label label-danger">' + item.service + '</span>';
+ }
+ });
+ } else if (table == 'general_syslog') {
+ $.each(data, function (i, item) {
+ item.message = escapeHtml(item.message);
+ var danger_class = ["emerg", "alert", "crit", "err"];
+ var warning_class = ["warning", "warn"];
+ var info_class = ["notice", "info", "debug"];
+ if (jQuery.inArray(item.priority, danger_class) !== -1) {
+ item.priority = '<span class="label label-danger">' + item.priority + '</span>';
+ }
+ else if (jQuery.inArray(item.priority, warning_class) !== -1) {
+ item.priority = '<span class="label label-warning">' + item.priority + '</span>';
+ }
+ else if (jQuery.inArray(item.priority, info_class) !== -1) {
+ item.priority = '<span class="label label-info">' + item.priority + '</span>';
+ }
+ });
+ }
+ return data
+ };
+ // Initial table drawings
draw_postfix_logs();
draw_autodiscover_logs();
draw_dovecot_logs();
@@ -695,7 +451,7 @@ jQuery(function($){
draw_fwd_hosts();
draw_relayhosts();
draw_rspamd_history();
-
+ // Relayhost
$('#testRelayhostModal').on('show.bs.modal', function (e) {
$('#test_relayhost_result').text("-");
button = $(e.relatedTarget)
@@ -703,16 +459,6 @@ jQuery(function($){
$('#relayhost_id').val(button.data('relayhost-id'));
}
})
-
- $('#showDKIMprivKey').on('show.bs.modal', function (e) {
- $('#priv_key_pre').text("-");
- p_related = $(e.relatedTarget)
- if (p_related != null) {
- var decoded_key = Base64.decode((p_related.data('priv-key')));
- $('#priv_key_pre').text(decoded_key);
- }
- })
-
$('#test_relayhost').on('click', function (e) {
e.preventDefault();
prev = $('#test_relayhost').text();
@@ -730,7 +476,41 @@ jQuery(function($){
}
});
})
+ // DKIM private key modal
+ $('#showDKIMprivKey').on('show.bs.modal', function (e) {
+ $('#priv_key_pre').text("-");
+ p_related = $(e.relatedTarget)
+ if (p_related != null) {
+ var decoded_key = Base64.decode((p_related.data('priv-key')));
+ $('#priv_key_pre').text(decoded_key);
+ }
+ })
+ $('.add_log_lines').on('click', function (e) {
+ e.preventDefault();
+ var log_table= $(this).data("table")
+ var new_nrows = ($(this).data("nrows") - 1)
+ var post_process = $(this).data("post-process")
+ var log_url = $(this).data("log-url")
+ if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
+ console.log("no data-table or data-nrows or log_url or data-post-process attr found");
+ return;
+ }
+ if (ft = FooTable.get($('#' + log_table))) {
+ var heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+ var ft_paging = ft.use(FooTable.Paging)
+ var load_rows = ft_paging.totalRows + '-' + (ft_paging.totalRows + new_nrows)
+ $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
+ if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
+ var rows = process_table_data(data, post_process);
+ var rows_now = (ft_paging.totalRows + data.length);
+ $(heading).children('.log-lines').text(rows_now)
+ mailcow_alert_box(data.length + lang.additional_rows, "success");
+ ft.rows.load(rows, true);
+ });
+ }
+ })
+ // App links
function add_table_row(table_id) {
var row = $('<tr />');
cols = '<td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required></td>';
@@ -739,17 +519,14 @@ jQuery(function($){
row.append(cols);
table_id.append(row);
}
-
$('#app_link_table').on('click', 'tr a', function (e) {
e.preventDefault();
$(this).parents('tr').remove();
});
-
$('#add_app_link_row').click(function() {
add_table_row($('#app_link_table'));
});
});
-
$(window).load(function(){
initial_width = $("#sidebar-admin").width();
$("#scrollbox").css("width", initial_width);
@@ -761,13 +538,10 @@ $(window).load(function(){
}
});
});
-
function resizeScrollbox() {
on_resize_width = $("#sidebar-admin").width();
$("#scrollbox").removeAttr("style");
$("#scrollbox").css("width", on_resize_width);
}
-
$(window).on('resize', resizeScrollbox);
$('a[data-toggle="tab"]').on('shown.bs.tab', resizeScrollbox);
-
diff --git a/data/web/js/api.js b/data/web/js/api.js
index 8935f856..23bf229e 100644
--- a/data/web/js/api.js
+++ b/data/web/js/api.js
@@ -61,6 +61,11 @@ $(document).ready(function() {
var id = $(this).data('id');
var api_url = $(this).data('api-url');
var api_attr = $(this).data('api-attr');
+ if (typeof $(this).data('api-reload-window') !== 'undefined') {
+ api_reload_window = $(this).data('api-reload-window');
+ } else {
+ api_reload_window = true;
+ }
// If clicked element #edit_selected is in a form with the same data-id as the button,
// we merge all input fields by {"name":"value"} into api-attr
if ($(this).closest("form").data('id') == id) {
@@ -106,10 +111,10 @@ $(document).ready(function() {
jsonp: false,
complete: function(data) {
var response = (data.responseText);
- // alert(response);
- // console.log(reponse.type);
- // console.log(reponse.msg);
- window.location = window.location.href.split("#")[0];
+ response_obj = JSON.parse(response);
+ if (api_reload_window === true) {
+ window.location = window.location.href.split("#")[0];
+ }
}
});
}
diff --git a/data/web/js/edit.js b/data/web/js/edit.js
index 057a9ecb..fbd5deca 100644
--- a/data/web/js/edit.js
+++ b/data/web/js/edit.js
@@ -10,6 +10,7 @@ $(document).ready(function() {
$("#textarea_alias_goto").removeAttr('disabled');
}
});
+ $("#script_data").numberedtextarea({allowTabChar: true});
});
jQuery(function($){
diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js
index c914f644..d55b1913 100644
--- a/data/web/js/mailbox.js
+++ b/data/web/js/mailbox.js
@@ -40,10 +40,61 @@ $(document).ready(function() {
});
// Log modal
- $('#logModal').on('show.bs.modal', function(e) {
- var logText = $(e.relatedTarget).data('log-text');
- $(e.currentTarget).find('#logText').html('<pre style="background:none;font-size:11px;line-height:1.1;border:0px">' + logText + '</pre>');
+ $('#syncjobLogModal').on('show.bs.modal', function(e) {
+ var syncjob_id = $(e.relatedTarget).data('syncjob-id');
+ $.ajax({
+ url: '/inc/ajax/syncjob_logs.php',
+ data: { id: syncjob_id },
+ dataType: 'text',
+ success: function(data){
+ $(e.currentTarget).find('#logText').text(data);
+ },
+ error: function(xhr, status, error) {
+ $(e.currentTarget).find('#logText').text(xhr.responseText);
+ }
+ });
+ });
+
+ // Sieve data modal
+ $('#sieveDataModal').on('show.bs.modal', function(e) {
+ var sieveScript = $(e.relatedTarget).data('sieve-script');
+ $(e.currentTarget).find('#sieveDataText').html('<pre style="font-size:14px;line-height:1.1">' + sieveScript + '</pre>');
+ });
+
+ // Set line numbers for textarea
+ $("#script_data").numberedtextarea({allowTabChar: true});
+ // Disable submit button on script change
+ $('#script_data').on('keyup', function() {
+ $('#add_filter_btns > #add_item').attr({"disabled": true});
+ $('#validation_msg').html('-');
+ });
+
+ // Validate script data
+ $("#validate_sieve").click(function( event ) {
+ event.preventDefault();
+ var script = $('#script_data').val();
+ $.ajax({
+ dataType: 'jsonp',
+ url: "/inc/ajax/sieve_validation.php",
+ type: "get",
+ data: { script: script },
+ complete: function(data) {
+ var response = (data.responseText);
+ response_obj = JSON.parse(response);
+ if (response_obj.type == "success") {
+ $('#add_filter_btns > #add_item').attr({"disabled": false});
+ }
+ mailcow_alert_box(response_obj.msg, response_obj.type);
+ },
+ });
});
+ // $(document).on('DOMNodeInserted', '#prefilter_table', function () {
+ // $("#active-script").closest('td').css('background-color','#b0f0a0');
+ // $("#inactive-script").closest('td').css('background-color','#b0f0a0');
+ // });
+
+
+
});
jQuery(function($){
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
@@ -87,7 +138,7 @@ jQuery(function($){
function draw_domain_table() {
ft_domain_table = FooTable.init('#domain_table', {
"columns": [
- {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"domain_name","title":lang.domain,"style":{"width":"250px"}},
{"name":"aliases","title":lang.aliases,"breakpoints":"xs sm"},
{"name":"mailboxes","title":lang.mailboxes},
@@ -152,7 +203,7 @@ jQuery(function($){
function draw_mailbox_table() {
ft_mailbox_table = FooTable.init('#mailbox_table', {
"columns": [
- {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"120px"},"title":lang.username},
{"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"120px"},"breakpoints":"xs sm"},
{"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
@@ -222,7 +273,7 @@ jQuery(function($){
function draw_resource_table() {
ft_resource_table = FooTable.init('#resource_table', {
"columns": [
- {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"description","title":lang.description,"style":{"width":"250px"}},
{"name":"kind","title":lang.kind},
{"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
@@ -267,7 +318,7 @@ jQuery(function($){
function draw_alias_table() {
ft_alias_table = FooTable.init('#alias_table', {
"columns": [
- {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}},
{"name":"goto","title":lang.target_address},
{"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
@@ -320,7 +371,7 @@ jQuery(function($){
function draw_aliasdomain_table() {
ft_aliasdomain_table = FooTable.init('#aliasdomain_table', {
"columns": [
- {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"alias_domain","title":lang.alias,"style":{"width":"250px"}},
{"name":"target_domain","title":lang.target_domain},
{"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
@@ -363,7 +414,7 @@ jQuery(function($){
function draw_sync_job_table() {
ft_syncjob_table = FooTable.init('#sync_job_table', {
"columns": [
- {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
{"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
{"name":"user2","title":lang.owner},
{"name":"server_w_port","title":"Server","breakpoints":"xs"},
@@ -376,14 +427,14 @@ jQuery(function($){
"empty": lang.empty,
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/syncjobs/all',
+ url: '/api/v1/get/syncjobs/all/no_log',
jsonp: false,
error: function () {
console.log('Cannot draw sync job table');
},
success: function (data) {
$.each(data, function (i, item) {
- item.log = '<a href="#logModal" data-toggle="modal" data-log-text="' + escapeHtml(item.returned_text) + '">Open logs</a>'
+ item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURI(item.id) + '">Open logs</a>'
item.exclude = '<code>' + item.exclude + '</code>'
item.server_w_port = item.user1 + '@' + item.host1 + ':' + item.port1;
item.action = '<div class="btn-group">' +
@@ -399,16 +450,76 @@ jQuery(function($){
"limit": 5,
"size": pagination_size
},
+ "filtering": {
+ "enabled": true,
+ "position": "left",
+ "placeholder": lang.filter_table
+ },
"sorting": {
"enabled": true
}
});
}
+ function draw_filter_table() {
+ ft_filter_table = FooTable.init('#filter_table', {
+ "columns": [
+ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
+ {"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
+ {"name":"active","style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
+ {"name":"filter_type","style":{"maxWidth":"80px","width":"80px"},"title":"Type"},
+ {"sorted": true,"name":"username","title":lang.owner,"style":{"maxWidth":"550px","width":"350px"}},
+ {"name":"script_desc","title":lang.description,"breakpoints":"xs"},
+ {"name":"script_data","title":"Script","breakpoints":"all"},
+ {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+ ],
+ "empty": lang.empty,
+ "rows": $.ajax({
+ dataType: 'json',
+ url: '/api/v1/get/filters/all',
+ jsonp: false,
+ error: function () {
+ console.log('Cannot draw filter table');
+ },
+ success: function (data) {
+ $.each(data, function (i, item) {
+ if (item.active_int == 1) {
+ item.active = '<span id="active-script" class="label label-success">' + lang.active + '</span>';
+ } else {
+ item.active = '<span id="inactive-script" class="label label-warning">' + lang.inactive + '</span>';
+ }
+ item.script_data = '<pre style="margin:0px">' + escapeHtml(item.script_data) + '</pre>'
+ item.filter_type = '<div class="label label-default">' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '</div>'
+ item.action = '<div class="btn-group">' +
+ '<a href="/edit.php?filter=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+ '<a href="#" id="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+ '</div>';
+ item.chkbox = '<input type="checkbox" data-id="filter_item" name="multi_select" value="' + item.id + '" />'
+ });
+ }
+ }),
+ "paging": {
+ "enabled": true,
+ "limit": 5,
+ "size": pagination_size
+ },
+ "filtering": {
+ "enabled": true,
+ "position": "left",
+ "placeholder": lang.filter_table
+ },
+ "sorting": {
+ "enabled": true
+ }
+ });
+ };
+
draw_domain_table();
draw_mailbox_table();
draw_resource_table();
draw_alias_table();
draw_aliasdomain_table();
draw_sync_job_table();
-});
\ No newline at end of file
+ draw_filter_table();
+
+});
diff --git a/data/web/js/numberedtextarea.min.js b/data/web/js/numberedtextarea.min.js
new file mode 100644
index 00000000..c99329f7
--- /dev/null
+++ b/data/web/js/numberedtextarea.min.js
@@ -0,0 +1 @@
+!function(e){function t(t,n){var a=e('<div class="numberedtextarea-wrapper"></div>').insertAfter(t);e(t).detach().appendTo(a)}function n(t,n){(t=e(t)).parents(".numberedtextarea-wrapper");var i=parseFloat(t.css("padding-left")),o=parseFloat(t.css("padding-top")),s=(parseFloat(t.css("padding-bottom")),e('<div class="numberedtextarea-line-numbers"></div>').insertAfter(t));t.css({paddingLeft:i+s.width()+"px"}).on("input propertychange change keyup paste",function(){a(t,n)}).on("scroll",function(){r(t,n)}),s.css({paddingLeft:i+"px",paddingTop:o+"px",lineHeight:t.css("line-height"),fontFamily:t.css("font-family"),width:s.width()-i+"px"}),t.trigger("change")}function a(t,n){var a=(t=e(t)).parent().find(".numberedtextarea-line-numbers"),r=t.val().split("\n").length,o=parseFloat(t.css("padding-bottom"));for(a.find(".numberedtextarea-number").remove(),i=1;i<=r;i++){var s=e('<div class="numberedtextarea-number numberedtextarea-number-'+i+'">'+i+"</div>").appendTo(a);i===r&&s.css("margin-bottom",o+"px")}}function r(t,n){(t=e(t)).parent().find(".numberedtextarea-line-numbers").scrollTop(t.scrollTop())}function o(e,t){if(e.focus(),"number"==typeof e.selectionStart){var n=e.value,a=e.selectionStart;e.value=n.slice(0,a)+t+n.slice(e.selectionEnd),e.selectionEnd=e.selectionStart=a+t.length}else if(void 0!==document.selection){var i=document.selection.createRange();i.text=t,i.collapse(!1),i.select()}}function s(t){e(t).keydown(function(e){if(9==e.which)return o(this,"\t"),!1}),e(t).keypress(function(e){if(9==e.which)return!1})}e.fn.numberedtextarea=function(a){var i=e.extend({color:null,borderColor:null,class:null,allowTabChar:!1},a);return this.each(function(){if("textarea"!==this.nodeName.toLowerCase())return console.log("This is not a <textarea>, so no way Jose..."),!1;t(this,i),n(this,i),i.allowTabChar&&e(this).allowTabChar()}),this},e.fn.allowTabChar=function(){return this.jquery&&this.each(function(){if(1==this.nodeType){var e=this.nodeName.toLowerCase();("textarea"==e||"input"==e&&"text"==this.type)&&s(this)}}),this}}(jQuery);
diff --git a/data/web/js/user.js b/data/web/js/user.js
index c091ac85..239507f4 100644
--- a/data/web/js/user.js
+++ b/data/web/js/user.js
@@ -1,13 +1,19 @@
$(document).ready(function() {
-
- // Log modal
- $('#logModal').on('show.bs.modal', function(e) {
- var logText = $(e.relatedTarget).data('log-text');
- $(e.currentTarget).find('#logText').html('<pre style="background:none;font-size:11px;line-height:1.1;border:0px">' + logText + '</pre>');
+ $('#syncjobLogModal').on('show.bs.modal', function(e) {
+ var syncjob_id = $(e.relatedTarget).data('syncjob-id');
+ $.ajax({
+ url: '/inc/ajax/syncjob_logs.php',
+ data: { id: syncjob_id },
+ dataType: 'text',
+ success: function(data){
+ $(e.currentTarget).find('#logText').text(data);
+ },
+ error: function(xhr, status, error) {
+ $(e.currentTarget).find('#logText').text(xhr.responseText);
+ }
+ });
});
-
});
-
jQuery(function($){
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
var entityMap = {
@@ -95,14 +101,14 @@ jQuery(function($){
"empty": lang.empty,
"rows": $.ajax({
dataType: 'json',
- url: '/api/v1/get/syncjobs/' + mailcow_cc_username,
+ url: '/api/v1/get/syncjobs/' + mailcow_cc_username + '/no_log',
jsonp: false,
error: function () {
console.log('Cannot draw sync job table');
},
success: function (data) {
$.each(data, function (i, item) {
- item.log = '<a href="#logModal" data-toggle="modal" data-log-text="' + escapeHtml(item.returned_text) + '">Open logs</a>'
+ item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURI(item.id) + '">Open logs</a>'
item.exclude = '<code>' + item.exclude + '</code>'
item.server_w_port = item.user1 + '@' + item.host1 + ':' + item.port1;
if (acl_data.syncjobs === 1) {
@@ -213,4 +219,27 @@ jQuery(function($){
draw_tla_table();
draw_wl_policy_mailbox_table();
draw_bl_policy_mailbox_table();
+
+ // Sieve data modal
+ $('#userFilterModal').on('show.bs.modal', function(e) {
+ $('#user_sieve_filter').text(lang.loading);
+ $.ajax({
+ dataType: 'json',
+ url: '/api/v1/get/active-user-sieve/' + mailcow_cc_username,
+ jsonp: false,
+ error: function () {
+ console.log('Cannot get active sieve script');
+ },
+ complete: function (data) {
+ if (data.responseText == '{}') {
+ $('#user_sieve_filter').text(lang.no_active_filter);
+ } else {
+ $('#user_sieve_filter').text(JSON.parse(data.responseText));
+ }
+ }
+ })
+ });
+ $('#userFilterModal').on('hidden.bs.modal', function () {
+ $('#user_sieve_filter').text(lang.loading);
+ });
});
\ No newline at end of file
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 0e4f9e87..0bc5e3a6 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -225,10 +225,10 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
- case "syncjob":
+ case "filter":
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
- if (mailbox('add', 'syncjob', $attr) === false) {
+ if (mailbox('add', 'filter', $attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
@@ -725,12 +725,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
case "logs":
switch ($object) {
case "dovecot":
- if (isset($extra) && !empty($extra)) {
- $extra = intval($extra);
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
$logs = get_logs('dovecot-mailcow', $extra);
}
else {
- $logs = get_logs('dovecot-mailcow', -1);
+ $logs = get_logs('dovecot-mailcow');
}
if (isset($logs) && !empty($logs)) {
echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -740,12 +741,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
}
break;
case "fail2ban":
- if (isset($extra) && !empty($extra)) {
- $extra = intval($extra);
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
$logs = get_logs('fail2ban-mailcow', $extra);
}
else {
- $logs = get_logs('fail2ban-mailcow', -1);
+ $logs = get_logs('fail2ban-mailcow');
}
if (isset($logs) && !empty($logs)) {
echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -755,12 +757,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
}
break;
case "postfix":
- if (isset($extra) && !empty($extra)) {
- $extra = intval($extra);
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
$logs = get_logs('postfix-mailcow', $extra);
}
else {
- $logs = get_logs('postfix-mailcow', -1);
+ $logs = get_logs('postfix-mailcow');
}
if (isset($logs) && !empty($logs)) {
echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -770,12 +773,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
}
break;
case "autodiscover":
- if (isset($extra) && !empty($extra)) {
- $extra = intval($extra);
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
$logs = get_logs('autodiscover-mailcow', $extra);
}
else {
- $logs = get_logs('autodiscover-mailcow', -1);
+ $logs = get_logs('autodiscover-mailcow');
}
if (isset($logs) && !empty($logs)) {
echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -785,12 +789,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
}
break;
case "sogo":
- if (isset($extra) && !empty($extra)) {
- $extra = intval($extra);
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
$logs = get_logs('sogo-mailcow', $extra);
}
else {
- $logs = get_logs('sogo-mailcow', -1);
+ $logs = get_logs('sogo-mailcow');
}
if (isset($logs) && !empty($logs)) {
echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -800,7 +805,14 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
}
break;
case "rspamd-history":
- $logs = get_logs('rspamd-history');
+ // 0 is first record, so empty is fine
+ if (isset($extra)) {
+ $extra = preg_replace('/[^\d\-]/i', '', $extra);
+ $logs = get_logs('rspamd-history', $extra);
+ }
+ else {
+ $logs = get_logs('rspamd-history');
+ }
if (isset($logs) && !empty($logs)) {
echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
@@ -863,7 +875,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
$syncjobs = mailbox('get', 'syncjobs', $mailbox);
if (!empty($syncjobs)) {
foreach ($syncjobs as $syncjob) {
- if ($details = mailbox('get', 'syncjob_details', $syncjob)) {
+ if (isset($extra)) {
+ $details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
+ }
+ else {
+ $details = mailbox('get', 'syncjob_details', $syncjob);
+ }
+ if ($details) {
$data[] = $details;
}
else {
@@ -890,7 +908,83 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
$syncjobs = mailbox('get', 'syncjobs', $object);
if (!empty($syncjobs)) {
foreach ($syncjobs as $syncjob) {
- if ($details = mailbox('get', 'syncjob_details', $syncjob)) {
+ if (isset($extra)) {
+ $details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
+ }
+ else {
+ $details = mailbox('get', 'syncjob_details', $syncjob);
+ }
+ if ($details) {
+ $data[] = $details;
+ }
+ else {
+ continue;
+ }
+ }
+ }
+ if (!isset($data) || empty($data)) {
+ echo '{}';
+ }
+ else {
+ echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ }
+ break;
+ }
+ break;
+ case "active-user-sieve":
+ if (isset($object)) {
+ $sieve_filter = mailbox('get', 'active_user_sieve', $object);
+ if (!empty($sieve_filter)) {
+ $data[] = $sieve_filter;
+ }
+ }
+ if (!isset($data) || empty($data)) {
+ echo '{}';
+ }
+ else {
+ echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ }
+ break;
+ case "filters":
+ switch ($object) {
+ case "all":
+ $domains = mailbox('get', 'domains');
+ if (!empty($domains)) {
+ foreach ($domains as $domain) {
+ $mailboxes = mailbox('get', 'mailboxes', $domain);
+ if (!empty($mailboxes)) {
+ foreach ($mailboxes as $mailbox) {
+ $filters = mailbox('get', 'filters', $mailbox);
+ if (!empty($filters)) {
+ foreach ($filters as $filter) {
+ if ($details = mailbox('get', 'filter_details', $filter)) {
+ $data[] = $details;
+ }
+ else {
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (!isset($data) || empty($data)) {
+ echo '{}';
+ }
+ else {
+ echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ }
+ }
+ else {
+ echo '{}';
+ }
+ break;
+
+ default:
+ $filters = mailbox('get', 'filters', $object);
+ if (!empty($filters)) {
+ foreach ($filters as $filter) {
+ if ($details = mailbox('get', 'filter_details', $filter)) {
$data[] = $details;
}
else {
@@ -1296,6 +1390,47 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
+ case "filter":
+ if (isset($_POST['items'])) {
+ $items = (array)json_decode($_POST['items'], true);
+ if (is_array($items)) {
+ if (mailbox('delete', 'filter', array('id' => $items)) === false) {
+ if (isset($_SESSION['return'])) {
+ echo json_encode($_SESSION['return']);
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Deletion of items/s failed'
+ ));
+ }
+ }
+ else {
+ if (isset($_SESSION['return'])) {
+ echo json_encode($_SESSION['return']);
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'success',
+ 'msg' => 'Task completed'
+ ));
+ }
+ }
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Cannot find id array in post data'
+ ));
+ }
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Cannot find items in post data'
+ ));
+ }
+ break;
case "fwdhost":
if (isset($_POST['items'])) {
$items = (array)json_decode($_POST['items'], true);
@@ -2102,6 +2237,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
+ case "filter":
+ if (isset($_POST['items']) && isset($_POST['attr'])) {
+ $items = (array)json_decode($_POST['items'], true);
+ $attr = (array)json_decode($_POST['attr'], true);
+ $postarray = array_merge(array('id' => $items), $attr);
+ if (is_array($postarray['id'])) {
+ if (mailbox('edit', 'filter', $postarray) === false) {
+ if (isset($_SESSION['return'])) {
+ echo json_encode($_SESSION['return']);
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Edit failed'
+ ));
+ }
+ exit();
+ }
+ else {
+ if (isset($_SESSION['return'])) {
+ echo json_encode($_SESSION['return']);
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'success',
+ 'msg' => 'Task completed'
+ ));
+ }
+ }
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Incomplete post data'
+ ));
+ }
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Incomplete post data'
+ ));
+ }
+ break;
case "resource":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php
index 681dca4e..bd329fa1 100644
--- a/data/web/lang/lang.de.php
+++ b/data/web/lang/lang.de.php
@@ -99,6 +99,9 @@ $lang['danger']['domain_not_empty'] = 'Kann nur leere Domains entfernen';
$lang['warning']['spam_alias_temp_error'] = 'Kann zur Zeit keinen Spam-Alias erstellen, bitte versuchen Sie es später noch einmal.';
$lang['danger']['spam_alias_max_exceeded'] = 'Maximale Anzahl an Spam-Alias-Adressen erreicht';
$lang['danger']['validity_missing'] = 'Bitte geben Sie eine Gültigkeitsdauer an';
+$lang['user']['loading'] = "Lade...";
+$lang['user']['active_sieve'] = "Aktiver Filter";
+$lang['user']['no_active_filter'] = "Kein aktiver Filter vorhanden";
$lang['user']['on'] = 'Ein';
$lang['user']['off'] = 'Aus';
$lang['user']['messages'] = "Nachrichten";
@@ -287,7 +290,7 @@ $lang['edit']['syncjob'] = 'Sync-Job bearbeiten';
$lang['edit']['save'] = 'Änderungen speichern';
$lang['edit']['username'] = 'Benutzername';
$lang['edit']['hostname'] = 'Servername';
-$lang['edit']['encryption'] = 'Verschlüsselungsmethode';
+$lang['edit']['encryption'] = 'Verschlüsselung';
$lang['edit']['maxage'] = 'Maximales Alter in Tagen einer Nachricht, die kopiert werden soll</br ><small>(0 = alle Nachrichten kopieren)</small>';
$lang['edit']['subfolder2'] = 'Ziel-Ordner<br><small>(leer = kein Unterordner)</small>';
$lang['edit']['mins_interval'] = 'Intervall (min)';
@@ -339,7 +342,7 @@ $lang['add']['syncjob_hint'] = 'Passwörter werden unverschlüsselt abgelegt!';
$lang['add']['hostname'] = 'Servername';
$lang['add']['port'] = 'Port';
$lang['add']['username'] = 'Benutzername';
-$lang['add']['enc_method'] = 'Verschlüsselungsmethode';
+$lang['add']['enc_method'] = 'Verschlüsselung';
$lang['add']['maxage'] = 'Maximales Alter von Nachrichten, welche vom Remote abgefragt werden (0 = Alter ignorieren)';
$lang['add']['subfolder2'] = 'Synchronisation in Unterordner am Ziel';
$lang['add']['mins_interval'] = 'Abrufintervall (Minuten)';
@@ -388,6 +391,21 @@ $lang['add']['password_repeat'] = 'Passwort (Wiederholung)';
$lang['add']['previous'] = 'Vorherige Seite';
$lang['add']['restart_sogo_hint'] = 'Der SOGo Container muss nach dem Hinzufügen einer neuen Domain neugestartet werden!';
$lang['add']['goto_null'] = 'Nachrichten sofort verwerfen';
+$lang['add']['validation_success'] = 'Erfolgreich validiert';
+$lang['add']['activate_filter_warn'] = 'Alle anderen Filter diesen Typs werden deaktiviert, falls dieses Script aktiv markiert wird.';
+$lang['add']['validate'] = 'Validieren';
+$lang['mailbox']['add_filter'] = 'Filter erstellen';
+$lang['add']['sieve_desc'] = 'Kurze Beschreibung';
+$lang['edit']['sieve_desc'] = 'Kurze Beschreibung';
+$lang['add']['sieve_type'] = 'Filtertyp';
+$lang['edit']['sieve_type'] = 'Filtertyp';
+$lang['mailbox']['set_prefilter'] = 'Als Prefilter markieren';
+$lang['mailbox']['set_postfilter'] = 'Als Postfilter markieren';
+$lang['mailbox']['filters'] = 'Filter';
+$lang['mailbox']['sync_jobs'] = 'Synchronisationen';
+$lang['mailbox']['inactive'] = 'Inaktiv';
+$lang['edit']['validate_save'] = 'Validieren und speichern';
+
$lang['login']['title'] = 'Anmeldung';
$lang['login']['administration'] = 'Administration';
@@ -425,6 +443,8 @@ $lang['tfa']['scan_qr_code'] = "Bitte scannen Sie jetzt den angezeigten QR-Code:
$lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel";
$lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens";
+$lang['admin']['no_new_rows'] = 'Keine weiteren Zeilen vorhanden';
+$lang['admin']['additional_rows'] = ' zusätzliche Zeilen geladen'; // parses to 'n additional rows were added'
$lang['admin']['private_key'] = 'Private Key';
$lang['admin']['import'] = 'Importieren';
$lang['admin']['import_private_key'] = 'Private Key importieren';
diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php
index c29d8e17..efa19dd2 100644
--- a/data/web/lang/lang.en.php
+++ b/data/web/lang/lang.en.php
@@ -101,6 +101,9 @@ $lang['danger']['domain_not_empty'] = "Cannot remove non-empty domain";
$lang['warning']['spam_alias_temp_error'] = "Temporary error: Cannot add spam alias, please try again later.";
$lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded";
$lang['danger']['validity_missing'] = 'Please assign a period of validity';
+$lang['user']['loading'] = "Loading...";
+$lang['user']['active_sieve'] = "Active filter";
+$lang['user']['no_active_filter'] = "No active filter available";
$lang['user']['on'] = "On";
$lang['user']['off'] = "Off";
$lang['user']['messages'] = "messages"; // "123 messages"
@@ -393,6 +396,21 @@ $lang['add']['password_repeat'] = 'Confirmation password (repeat)';
$lang['add']['previous'] = 'Previous page';
$lang['add']['restart_sogo_hint'] = 'You will need to restart the SOGo service container after adding a new domain!';
$lang['add']['goto_null'] = 'Silently discard mail';
+$lang['add']['validation_success'] = 'Validated successfully';
+$lang['add']['activate_filter_warn'] = 'All other filters will be deactivated, when active is checked.';
+$lang['add']['validate'] = 'Validate';
+$lang['mailbox']['add_filter'] = 'Add filter';
+$lang['add']['sieve_desc'] = 'Short description';
+$lang['edit']['sieve_desc'] = 'Short description';
+$lang['add']['sieve_type'] = 'Filter type';
+$lang['edit']['sieve_type'] = 'Filter type';
+$lang['mailbox']['set_prefilter'] = 'Mark as prefilter';
+$lang['mailbox']['set_postfilter'] = 'Mark as postfilter';
+$lang['mailbox']['filters'] = 'Filters';
+$lang['mailbox']['sync_jobs'] = 'Sync jobs';
+$lang['mailbox']['inactive'] = 'Inactive';
+$lang['edit']['validate_save'] = 'Validate and save';
+
$lang['login']['title'] = 'Login';
$lang['login']['administration'] = 'Administration';
@@ -430,6 +448,8 @@ $lang['tfa']['scan_qr_code'] = "Please scan the following code with your authent
$lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes";
$lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token";
+$lang['admin']['no_new_rows'] = 'No further rows available';
+$lang['admin']['additional_rows'] = ' additional rows were added'; // parses to 'n additional rows were added'
$lang['admin']['private_key'] = 'Private key';
$lang['admin']['import'] = 'Import';
$lang['admin']['import_private_key'] = 'Import private key';
diff --git a/data/web/mailbox.php b/data/web/mailbox.php
index 0325e7f7..a2d72f22 100644
--- a/data/web/mailbox.php
+++ b/data/web/mailbox.php
@@ -19,7 +19,8 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<li role="presentation"><a href="#tab-domain-aliases" aria-controls="tab-domain-aliases" role="tab" data-toggle="tab"><?=$lang['mailbox']['domain_aliases'];?></a></li>
</ul>
</li>
- <li role="presentation"><a href="#tab-syncjobs" aria-controls="tab-resources" role="tab" data-toggle="tab">Sync jobs</a></li>
+ <li role="presentation"><a href="#tab-syncjobs" aria-controls="tab-syncjobs" role="tab" data-toggle="tab"><?=$lang['mailbox']['sync_jobs'];?></a></li>
+ <li role="presentation"><a href="#tab-filters" aria-controls="tab-filters" role="tab" data-toggle="tab"><?=$lang['mailbox']['filters'];?></a></li>
</ul>
<div class="row">
@@ -151,7 +152,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<div role="tabpanel" class="tab-pane" id="tab-syncjobs">
<div class="panel panel-default">
<div class="panel-heading">
- <h3 class="panel-title">Sync jobs</h3>
+ <h3 class="panel-title"><?=$lang['mailbox']['sync_jobs'];?></h3>
</div>
<div class="table-responsive">
<table class="table table-striped" id="sync_job_table"></table>
@@ -169,6 +170,37 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addSyncJobModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['create_syncjob'];?></a>
</div>
</div>
+ </div>
+ </div>
+
+ <div role="tabpanel" class="tab-pane" id="tab-filters">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title"><?=$lang['mailbox']['filters'];?></h3>
+ </div>
+ <p style="margin:10px" class="help-block">You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.</p>
+ <p style="margin:10px" class="help-block">Each filter will be processed in the described order. A failed script will not stop processing of further scripts.</a></p>
+ <p style="margin:10px" class="help-block">Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a></p>
+ <div class="table-responsive">
+ <table class="table table-striped" id="filter_table"></table>
+ </div>
+ <div class="mass-actions-mailbox">
+ <div class="btn-group">
+ <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="filter_item" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
+ <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
+ <ul class="dropdown-menu">
+ <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+ <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+ <li role="separator" class="divider"></li>
+ <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"prefilter"}' href="#"><?=$lang['mailbox']['set_prefilter'];?></a></li>
+ <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#"><?=$lang['mailbox']['set_postfilter'];?></a></li>
+ <li role="separator" class="divider"></li>
+ <li><a id="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="filter_item" data-api-url='delete/filter' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+ </ul>
+ <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addFilterModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_filter'];?></a>
+ </div>
+ </div>
+ </div>
</div>
</div> <!-- /tab-content -->
diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php
index 3ce30a3f..61dd747d 100644
--- a/data/web/modals/mailbox.php
+++ b/data/web/modals/mailbox.php
@@ -314,7 +314,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
<h3 class="modal-title"><?=$lang['add']['syncjob'];?></h3>
</div>
<div class="modal-body">
- <p><?=$lang['add']['syncjob_hint'];?></p>
+ <p class="help-block"><?=$lang['add']['syncjob_hint'];?></p>
<form class="form-horizontal" role="form" data-id="add_syncjob">
<div class="form-group">
<label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
@@ -437,12 +437,81 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
</div>
</div>
</div><!-- add sync job modal -->
+<!-- add add_filter modal -->
+<div class="modal fade" id="addFilterModalAdmin" tabindex="-1" role="dialog" aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
+ <h3 class="modal-title">Filter</h3>
+ </div>
+ <div class="modal-body">
+ <form class="form-horizontal" role="form" data-id="add_filter">
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
+ <div class="col-sm-10">
+ <select id="addSelectUsername" name="username" id="username" required>
+ <?php
+ $domains = mailbox('get', 'domains');
+ if (!empty($domains)) {
+ foreach ($domains as $domain) {
+ $mailboxes = mailbox('get', 'mailboxes', $domain);
+ foreach ($mailboxes as $mailbox) {
+ echo "<option>".htmlspecialchars($mailbox)."</option>";
+ }
+ }
+ }
+ ?>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="filter_type"><?=$lang['add']['sieve_type'];?>:</label>
+ <div class="col-sm-10">
+ <select id="addFilterType" name="filter_type" id="filter_type" required>
+ <option value="prefilter">Prefilter</option>
+ <option value="postfilter">Postfilter</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="script_desc"><?=$lang['add']['sieve_desc'];?>:</label>
+ <div class="col-sm-10">
+ <input type="text" class="form-control" name="script_desc" id="script_desc" required maxlength="255">
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2" for="script_data">Script:</label>
+ <div class="col-sm-10">
+ <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control" rows="20" id="script_data" name="script_data" required></textarea>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10">
+ <p class="help-block"><?=$lang['add']['activate_filter_warn'];?></p>
+ <div class="checkbox">
+ <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10" id="add_filter_btns">
+ <button class="btn btn-default" id="validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
+ <button class="btn btn-success" id="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+</div><!-- add add_filter modal -->
<!-- log modal -->
-<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
- <div class="modal-dialog" style="width:90%" role="document">
+<div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
+ <div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
+ <div class="modal-header"><h4 class="modal-title">Log</h4></div>
<div class="modal-body">
- <span id="logText"></span>
+ <textarea class="form-control" rows="20" id="logText"></textarea>
</div>
</div>
</div>
diff --git a/data/web/modals/user.php b/data/web/modals/user.php
index e237a74b..9b4394f3 100644
--- a/data/web/modals/user.php
+++ b/data/web/modals/user.php
@@ -112,11 +112,12 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
</div>
</div><!-- add sync job modal -->
<!-- log modal -->
-<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
- <div class="modal-dialog" style="width:90%" role="document">
+<div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
+ <div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
+ <div class="modal-header"><h4 class="modal-title">Log</h4></div>
<div class="modal-body">
- <span id="logText"></span>
+ <textarea class="form-control" rows="20" id="logText"></textarea>
</div>
</div>
</div>
@@ -156,4 +157,18 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
</div>
</div>
</div>
-</div><!-- pw change modal -->
\ No newline at end of file
+</div><!-- pw change modal -->
+<!-- sieve filter modal -->
+<div class="modal fade" id="userFilterModal" tabindex="-1" role="dialog" aria-labelledby="pwChangeModalLabel">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
+ <h3 class="modal-title"><?=$lang['user']['active_sieve'];?></h3>
+ </div>
+ <div class="modal-body">
+ <pre id="user_sieve_filter"></pre>
+ </div>
+ </div>
+ </div>
+</div><!-- sieve filter modal -->
\ No newline at end of file
diff --git a/data/web/user.php b/data/web/user.php
index 50583515..5c5d6720 100644
--- a/data/web/user.php
+++ b/data/web/user.php
@@ -94,6 +94,13 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
</div>
</div>
<hr>
+ <div class="row">
+ <div class="col-md-3 col-xs-5 text-right"> <span class="glyphicon glyphicon-filter"></span></div>
+ <div class="col-md-9 col-xs-7">
+ <p><a href="#userFilterModal" data-toggle="modal">[Show active user sieve filter]</a></p>
+ </div>
+ </div>
+ <hr>
<?php // Get user information about aliases
$user_get_alias_details = user_get_alias_details($username);
?>