Browse Source

[Watchdog] Use stackoverflow.com for DNS check
[Git] Ignore mail_plugins*
[Dovecot] Read mail_plugins from dynamically generated file
[Dovecot] Encrypt FTS
[Dovecot] Add break_imap_seach option to Solr
[Web] Add ability to send quarantine notification mails
[Web] Minor style fixes
[Web] Add new MAILBOX_DEFAULT_ATTRIBUTES (doc updates, anyone? :-( )
[Web] Use rcpt_smtp if rcpt_mime is not set
[Web] Other minor fixes

andryyy 6 years ago
parent
commit
07392b7437

+ 1 - 0
.gitignore

@@ -23,6 +23,7 @@ data/conf/nginx/*.conf
 data/conf/nginx/*.custom
 data/conf/nginx/*.custom
 data/conf/nginx/*.bak
 data/conf/nginx/*.bak
 data/conf/dovecot/acl_anyone
 data/conf/dovecot/acl_anyone
+data/conf/dovecot/mail_plugins*
 data/conf/dovecot/extra.conf
 data/conf/dovecot/extra.conf
 data/conf/rspamd/custom/*
 data/conf/rspamd/custom/*
 data/conf/portainer/
 data/conf/portainer/

+ 1 - 1
data/Dockerfiles/watchdog/watchdog.sh

@@ -143,7 +143,7 @@ unbound_checks() {
     cat /dev/null > /tmp/unbound-mailcow
     cat /dev/null > /tmp/unbound-mailcow
     host_ip=$(get_container_ip unbound-mailcow)
     host_ip=$(get_container_ip unbound-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H google.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
+    /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
     DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
     if [[ -z ${DNSSEC} ]]; then
     if [[ -z ${DNSSEC} ]]; then
       echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
       echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2

+ 7 - 4
data/conf/dovecot/dovecot.conf

@@ -20,7 +20,7 @@ disable_plaintext_auth = yes
 login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
 login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
 mail_home = /var/vmail/%d/%n
 mail_home = /var/vmail/%d/%n
 mail_location = maildir:~/
 mail_location = maildir:~/
-mail_plugins = quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr
+mail_plugins = </usr/local/etc/dovecot/mail_plugins
 mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
 mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
 mail_attachment_dir = /var/attachments
 mail_attachment_dir = /var/attachments
 mail_attachment_min_size = 128k
 mail_attachment_min_size = 128k
@@ -290,12 +290,12 @@ userdb {
   skip = found
   skip = found
 }
 }
 protocol imap {
 protocol imap {
+  mail_plugins = </usr/local/etc/dovecot/mail_plugins_imap
   imap_metadata = yes
   imap_metadata = yes
-  mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr
 }
 }
 mail_attribute_dict = file:%h/dovecot-attributes
 mail_attribute_dict = file:%h/dovecot-attributes
 protocol lmtp {
 protocol lmtp {
-  mail_plugins = quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr
+  mail_plugins = </usr/local/etc/dovecot/mail_plugins_lmtp
   auth_socket_path = /usr/local/var/run/dovecot/auth-master
   auth_socket_path = /usr/local/var/run/dovecot/auth-master
 }
 }
 protocol sieve {
 protocol sieve {
@@ -308,7 +308,10 @@ plugin {
   acl = vfile
   acl = vfile
   fts = solr
   fts = solr
   fts_autoindex = yes
   fts_autoindex = yes
-  fts_solr = url=http://solr:8983/solr/dovecot/
+  fts_solr = break-imap-search url=http://solr:8983/solr/dovecot/
+  fts_index_fs = crypt:set_prefix=fscrypt_index:posix:
+  fscrypt_index_public_key = </mail_crypt/ecpubkey.pem
+  fscrypt_index_private_key =  </mail_crypt/ecprivkey.pem
   quota = dict:Userquota::proxy::sqlquota
   quota = dict:Userquota::proxy::sqlquota
   quota_rule2 = Trash:storage=+100%%
   quota_rule2 = Trash:storage=+100%%
   sieve = /var/vmail/sieve/%u.sieve
   sieve = /var/vmail/sieve/%u.sieve

+ 30 - 5
data/web/admin.php

@@ -7,6 +7,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $tfa_data = get_tfa();
 $tfa_data = get_tfa();
 ?>
 ?>
 <div class="container">
 <div class="container">
+
   <ul class="nav nav-tabs" role="tablist">
   <ul class="nav nav-tabs" role="tablist">
     <li role="presentation" class="active"><a href="#tab-access" aria-controls="tab-access" role="tab" data-toggle="tab"><?=$lang['admin']['access'];?></a></li>
     <li role="presentation" class="active"><a href="#tab-access" aria-controls="tab-access" role="tab" data-toggle="tab"><?=$lang['admin']['access'];?></a></li>
     <li role="presentation"><a href="#tab-config" aria-controls="tab-config" role="tab" data-toggle="tab"><?=$lang['admin']['configuration'];?></a></li>
     <li role="presentation"><a href="#tab-config" aria-controls="tab-config" role="tab" data-toggle="tab"><?=$lang['admin']['configuration'];?></a></li>
@@ -15,6 +16,8 @@ $tfa_data = get_tfa();
     <li role="presentation"><a href="#tab-mailq" aria-controls="tab-mailq" role="tab" data-toggle="tab"><?=$lang['admin']['queue_manager'];?></a></li>
     <li role="presentation"><a href="#tab-mailq" aria-controls="tab-mailq" role="tab" data-toggle="tab"><?=$lang['admin']['queue_manager'];?></a></li>
   </ul>
   </ul>
 
 
+  <div class="row">
+  <div class="col-md-12">
   <div class="tab-content" style="padding-top:20px">
   <div class="tab-content" style="padding-top:20px">
   <div role="tabpanel" class="tab-pane active" id="tab-access">
   <div role="tabpanel" class="tab-pane active" id="tab-access">
     <div class="panel panel-danger">
     <div class="panel panel-danger">
@@ -272,8 +275,6 @@ $tfa_data = get_tfa();
     </div>
     </div>
   </div>
   </div>
 
 
-
-
   <div role="tabpanel" class="tab-pane" id="tab-config">
   <div role="tabpanel" class="tab-pane" id="tab-config">
     <div class="row">
     <div class="row">
     <div id="sidebar-admin" class="col-sm-2 hidden-xs">
     <div id="sidebar-admin" class="col-sm-2 hidden-xs">
@@ -631,7 +632,28 @@ $tfa_data = get_tfa();
           <div class="row">
           <div class="row">
             <div class="col-sm-6">
             <div class="col-sm-6">
               <div class="form-group">
               <div class="form-group">
-                <label for="release_format"><?=$lang['admin']['quarantine_release_format'];?></label>
+                <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label>
+                <input type="text" class="form-control" name="sender" value="<?=$q_data['sender'];?>" placeholder="quarantine@localhost">
+              </div>
+            </div>
+            <div class="col-sm-6">
+              <div class="form-group">
+                <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label>
+                <input type="text" class="form-control" name="subject" value="<?=$q_data['subject'];?>" placeholder="Spam Quarantine Notification">
+              </div>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-sm-12">
+              <label for="html"><?=$lang['admin']['quarantine_notification_html'];?></label>
+              <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="html"><?=$q_data['html'];?></textarea>
+              <br>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-sm-6">
+              <div class="form-group">
+                <label for="release_format"><?=$lang['admin']['quarantine_release_format'];?>:</label>
                 <select data-width="100%" name="release_format" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
                 <select data-width="100%" name="release_format" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
                   <option <?=($q_data['release_format'] == 'raw') ? 'selected' : null;?> value="raw"><?=$lang['admin']['quarantine_release_format_raw'];?></option>
                   <option <?=($q_data['release_format'] == 'raw') ? 'selected' : null;?> value="raw"><?=$lang['admin']['quarantine_release_format_raw'];?></option>
                   <option <?=($q_data['release_format'] == 'attachment') ? 'selected' : null;?> value="attachment"><?=$lang['admin']['quarantine_release_format_att'];?></option>
                   <option <?=($q_data['release_format'] == 'attachment') ? 'selected' : null;?> value="attachment"><?=$lang['admin']['quarantine_release_format_att'];?></option>
@@ -640,7 +662,7 @@ $tfa_data = get_tfa();
             </div>
             </div>
             <div class="col-sm-6">
             <div class="col-sm-6">
               <div class="form-group">
               <div class="form-group">
-                <label for="exclude_domains"><?=$lang['admin']['quarantine_exclude_domains'];?></label><br />
+                <label for="exclude_domains"><?=$lang['admin']['quarantine_exclude_domains'];?>:</label><br />
                 <select data-width="100%" name="exclude_domains" class="selectpicker" title="<?=$lang['tfa']['select'];?>" multiple>
                 <select data-width="100%" name="exclude_domains" class="selectpicker" title="<?=$lang['tfa']['select'];?>" multiple>
                 <?php
                 <?php
                 foreach (array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')) as $domain):
                 foreach (array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')) as $domain):
@@ -663,7 +685,7 @@ $tfa_data = get_tfa();
       <div class="panel-heading">Rspamd settings map</div>
       <div class="panel-heading">Rspamd settings map</div>
       <div class="panel-body">
       <div class="panel-body">
       <legend>Active settings map</legend>
       <legend>Active settings map</legend>
-      <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control" rows="20" id="settings_map" name="settings_map" readonly><?=file_get_contents('http://nginx:8081/settings.php');?></textarea>
+      <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="settings_map" readonly><?=file_get_contents('http://nginx:8081/settings.php');?></textarea>
       <hr>
       <hr>
       <?php $rsettings = rsettings('get'); ?>
       <?php $rsettings = rsettings('get'); ?>
         <form class="form" data-id="rsettings" role="form" method="post">
         <form class="form" data-id="rsettings" role="form" method="post">
@@ -970,6 +992,9 @@ $tfa_data = get_tfa();
     </div>
     </div>
   </div>
   </div>
 
 
+  </div> <!-- /tab-content -->
+  </div> <!-- /col-md-12 -->
+  </div> <!-- /row -->
 </div> <!-- /container -->
 </div> <!-- /container -->
 <?php
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/admin.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/admin.php';

+ 5 - 5
data/web/css/admin.css

@@ -24,6 +24,11 @@ body.modal-open {
   overflow-y:scroll;
   overflow-y:scroll;
   padding-right: inherit !important;
   padding-right: inherit !important;
 }
 }
+@media (min-width: 992px) {
+  .container {
+      width: 80%;
+  }
+}
 .mass-actions-admin {
 .mass-actions-admin {
   user-select: none;
   user-select: none;
   padding:10px 0 10px 0;
   padding:10px 0 10px 0;
@@ -60,11 +65,6 @@ body.modal-open {
 .nav-tabs>li>a {
 .nav-tabs>li>a {
   z-index: 1;
   z-index: 1;
 }
 }
-#settings_map {
-  font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
-  font-size:9pt;
-  background:transparent;
-}
 .table-condensed .input-sm {
 .table-condensed .input-sm {
   width: 100%!important;  
   width: 100%!important;  
 }
 }

+ 0 - 1
data/web/css/debug.css

@@ -40,5 +40,4 @@ table.footable>tbody>tr.footable-empty>td {
 }
 }
 tbody {
 tbody {
   font-size:14px;
   font-size:14px;
-  color:#333;
 }
 }

+ 7 - 3
data/web/css/mailcow.css

@@ -19,8 +19,8 @@
 @font-face {
 @font-face {
   font-family: 'Source Sans Pro';
   font-family: 'Source Sans Pro';
   font-style: italic;
   font-style: italic;
-  font-weight: 700;
-  src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldIt'), url('../fonts/SourceSansPro-BoldIt.woff2') format('woff2');
+  font-weight: 300;
+  src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url('../fonts/SourceSansPro-Italic.woff2') format('woff2');
 }
 }
 #maxmsgsize { min-width: 80px; }
 #maxmsgsize { min-width: 80px; }
 #slider1 .slider-selection {
 #slider1 .slider-selection {
@@ -42,6 +42,10 @@
 .btn {
 .btn {
   text-transform: none;
   text-transform: none;
 }
 }
+.textarea-code {
+  font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
+  background:transparent !important;
+}
 .navbar-nav {
 .navbar-nav {
   margin: 0;
   margin: 0;
 }
 }
@@ -157,4 +161,4 @@ nav .glyphicon {
 }
 }
 .full-width-select {
 .full-width-select {
   width: 100%!important;  
   width: 100%!important;  
-}
+}

+ 1 - 1
data/web/css/numberedtextarea.min.css

@@ -1 +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}
+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}

+ 14 - 0
data/web/css/quarantine.css

@@ -35,3 +35,17 @@ table.footable>tbody>tr.footable-empty>td {
 .inputMissingAttr {
 .inputMissingAttr {
   border-color: #FF4136;
   border-color: #FF4136;
 }
 }
+.dot-danger {
+  height: 10px;
+  width: 10px;
+  background-color: #ff4136;
+  border-radius: 50%;
+  display: inline-block;
+}
+.dot-neutral {
+  height: 10px;
+  width: 10px;
+  background-color: #d4d4d4;
+  border-radius: 50%;
+  display: inline-block;
+}

+ 15 - 1
data/web/edit.php

@@ -49,6 +49,20 @@ if (isset($_SESSION['mailcow_cc_role'])) {
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
+            <hr>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="private_"><?=$lang['edit']['private_comment'];?></label>
+              <div class="col-sm-10">
+                <input maxlength="160" class="form-control" type="text" name="private_comment" value="<?=htmlspecialchars($result['private_comment']);?>" />
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="public_comment"><?=$lang['edit']['public_comment'];?></label>
+              <div class="col-sm-10">
+                <input maxlength="160" class="form-control" type="text" name="public_comment" value="<?=htmlspecialchars($result['public_comment']);?>" />
+              </div>
+            </div>
+            <hr>
             <div class="form-group">
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
               <div class="col-sm-offset-2 col-sm-10">
                 <div class="checkbox">
                 <div class="checkbox">
@@ -1165,7 +1179,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
             <div class="form-group">
               <label class="control-label col-sm-2" for="script_data">Script:</label>
               <label class="control-label col-sm-2" for="script_data">Script:</label>
               <div class="col-sm-10">
               <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>
+                <textarea spellcheck="false" autocorrect="off" autocapitalize="none" class="form-control textarea-code" rows="20" id="script_data" name="script_data" required><?=$result['script_data'];?></textarea>
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">

BIN
data/web/fonts/SourceSansPro-Italic.woff2


+ 3 - 0
data/web/inc/ajax/qitem_details.php

@@ -74,6 +74,9 @@ if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) {
       }
       }
     }
     }
     if (isset($_GET['att'])) {
     if (isset($_GET['att'])) {
+      if ($_SESSION['acl']['quarantine_attachments'] == 0) {
+        exit(json_encode('Forbidden'));
+      }
       $dl_id = intval($_GET['att']);
       $dl_id = intval($_GET['att']);
       $dl_filename = $data['attachments'][$dl_id][0];
       $dl_filename = $data['attachments'][$dl_id][0];
       if (!is_dir($tmpdir . $dl_filename) && file_exists($tmpdir . $dl_filename)) {
       if (!is_dir($tmpdir . $dl_filename) && file_exists($tmpdir . $dl_filename)) {

+ 10 - 9
data/web/inc/functions.inc.php

@@ -611,6 +611,8 @@ function edit_user_account($_data) {
 function user_get_alias_details($username) {
 function user_get_alias_details($username) {
 	global $lang;
 	global $lang;
 	global $pdo;
 	global $pdo;
+  $data['direct_aliases'] = false;
+  $data['shared_aliases'] = false;
   if ($_SESSION['mailcow_cc_role'] == "user") {
   if ($_SESSION['mailcow_cc_role'] == "user") {
     $username	= $_SESSION['mailcow_cc_username'];
     $username	= $_SESSION['mailcow_cc_username'];
   }
   }
@@ -618,7 +620,7 @@ function user_get_alias_details($username) {
     return false;
     return false;
   }
   }
   $data['address'] = $username;
   $data['address'] = $username;
-  $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') AS `shared_aliases` FROM `alias`
+  $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
     WHERE `goto` REGEXP :username_goto
     WHERE `goto` REGEXP :username_goto
     AND `address` NOT LIKE '@%'
     AND `address` NOT LIKE '@%'
     AND `goto` != :username_goto2
     AND `goto` != :username_goto2
@@ -630,9 +632,10 @@ function user_get_alias_details($username) {
     ));
     ));
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   while ($row = array_shift($run)) {
   while ($row = array_shift($run)) {
-    $data['shared_aliases'] = $row['shared_aliases'];
+    $data['shared_aliases'][] = $row['shared_aliases'];
   }
   }
-  $stmt = $pdo->prepare("SELECT GROUP_CONCAT(`address` SEPARATOR ', ') AS `direct_aliases` FROM `alias`
+
+  $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
     WHERE `goto` = :username_goto
     WHERE `goto` = :username_goto
     AND `address` NOT LIKE '@%'
     AND `address` NOT LIKE '@%'
     AND `address` != :username_address");
     AND `address` != :username_address");
@@ -640,21 +643,19 @@ function user_get_alias_details($username) {
     array(
     array(
     ':username_goto' => $username,
     ':username_goto' => $username,
     ':username_address' => $username
     ':username_address' => $username
-    ));
+  ));
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   while ($row = array_shift($run)) {
   while ($row = array_shift($run)) {
-    $data['direct_aliases'][] = $row['direct_aliases'];
+    $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
   }
   }
-  $stmt = $pdo->prepare("SELECT GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ', ') AS `ad_alias` FROM `mailbox`
+  $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
     LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
     LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
       WHERE `username` = :username ;");
       WHERE `username` = :username ;");
   $stmt->execute(array(':username' => $username));
   $stmt->execute(array(':username' => $username));
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   while ($row = array_shift($run)) {
   while ($row = array_shift($run)) {
-    $data['direct_aliases'][] = $row['ad_alias'];
+    $data['direct_aliases'][$row['ad_alias']]['public_comment'] = '↪ ' . $row['alias_domain'];
   }
   }
-  $data['direct_aliases'] = implode(', ', array_filter($data['direct_aliases']));
-  $data['direct_aliases'] = empty($data['direct_aliases']) ? '&#10008;' : $data['direct_aliases'];
   $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
   $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
   $stmt->execute(array(':username' => $username));
   $stmt->execute(array(':username' => $username));
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);

+ 104 - 2
data/web/inc/functions.mailbox.inc.php

@@ -453,6 +453,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $goto_null = intval($_data['goto_null']);
           $goto_null = intval($_data['goto_null']);
           $goto_spam = intval($_data['goto_spam']);
           $goto_spam = intval($_data['goto_spam']);
           $goto_ham = intval($_data['goto_ham']);
           $goto_ham = intval($_data['goto_ham']);
+          $private_comment = $_data['private_comment'];
+          $public_comment = $_data['public_comment'];
+          if (strlen($private_comment) > 160 | strlen($public_comment) > 160){
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'comment_too_long'
+            );
+            return false;
+          } 
           if (empty($addresses[0])) {
           if (empty($addresses[0])) {
             $_SESSION['return'][] = array(
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'type' => 'danger',
@@ -593,10 +603,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               );
               return false;
               return false;
             }
             }
-            $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`)
+            $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `active`)
               VALUES (:address, :goto, :domain, :active)");
               VALUES (:address, :goto, :domain, :active)");
             if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
             if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
               $stmt->execute(array(
               $stmt->execute(array(
+                ':address' => '@'.$domain,
+                ':public_comment' => $public_comment,
+                ':private_comment' => $private_comment,
                 ':address' => '@'.$domain,
                 ':address' => '@'.$domain,
                 ':goto' => $goto,
                 ':goto' => $goto,
                 ':domain' => $domain,
                 ':domain' => $domain,
@@ -606,6 +619,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             else {
             else {
               $stmt->execute(array(
               $stmt->execute(array(
                 ':address' => $address,
                 ':address' => $address,
+                ':public_comment' => $public_comment,
+                ':private_comment' => $private_comment,
                 ':goto' => $goto,
                 ':goto' => $goto,
                 ':domain' => $domain,
                 ':domain' => $domain,
                 ':active' => $active
                 ':active' => $active
@@ -753,7 +768,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               'tls_enforce_in' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'])),
               'tls_enforce_in' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'])),
               'tls_enforce_out' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'])),
               'tls_enforce_out' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'])),
               'sogo_access' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'])),
               'sogo_access' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'])),
-              'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'])
+              'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
+              'quarantine_notification' => strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'])
             )
             )
           );
           );
           if (!is_valid_domain_name($domain)) {
           if (!is_valid_domain_name($domain)) {
@@ -1148,6 +1164,65 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             );
           }
           }
         break;
         break;
+        case 'quarantine_notification':
+          if (!is_array($_data['username'])) {
+            $usernames = array();
+            $usernames[] = $_data['username'];
+          }
+          else {
+            $usernames = $_data['username'];
+          }
+          if (!isset($_SESSION['acl']['quarantine_notification']) || $_SESSION['acl']['quarantine_notification'] != "1" ) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          foreach ($usernames as $username) {
+            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'access_denied'
+              );
+              continue;
+            }
+            $is_now = mailbox('get', 'quarantine_notification', $username);
+            if (!empty($is_now)) {
+              $quarantine_notification = (isset($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
+            }
+            else {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'access_denied'
+              );
+              continue;
+            }
+            if (!in_array($quarantine_notification, array('never', 'hourly', 'daily', 'weekly'))) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'access_denied'
+              );
+              continue;
+            } 
+            $stmt = $pdo->prepare("UPDATE `mailbox`
+              SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', :quarantine_notification)
+                WHERE `username` = :username");
+            $stmt->execute(array(
+              ':quarantine_notification' => $quarantine_notification,
+              ':username' => $username
+            ));
+            $_SESSION['return'][] = array(
+              'type' => 'success',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => array('mailbox_modified', $username)
+            );
+          }
+        break;
         case 'spam_score':
         case 'spam_score':
           if (!is_array($_data['username'])) {
           if (!is_array($_data['username'])) {
             $usernames = array();
             $usernames = array();
@@ -1587,6 +1662,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
               $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
               $goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
               $goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
               $goto_ham = (isset($_data['goto_ham'])) ? intval($_data['goto_ham']) : 0;
               $goto_ham = (isset($_data['goto_ham'])) ? intval($_data['goto_ham']) : 0;
+              $public_comment = (isset($_data['public_comment'])) ? $_data['public_comment'] : $is_now['public_comment'];
+              $private_comment = (isset($_data['private_comment'])) ? $_data['private_comment'] : $is_now['private_comment'];
               $goto = (!empty($_data['goto'])) ? $_data['goto'] : $is_now['goto'];
               $goto = (!empty($_data['goto'])) ? $_data['goto'] : $is_now['goto'];
               $address = (!empty($_data['address'])) ? $_data['address'] : $is_now['address'];
               $address = (!empty($_data['address'])) ? $_data['address'] : $is_now['address'];
             }
             }
@@ -1703,11 +1780,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             if (!empty($goto)) {
             if (!empty($goto)) {
               $stmt = $pdo->prepare("UPDATE `alias` SET
               $stmt = $pdo->prepare("UPDATE `alias` SET
                 `address` = :address,
                 `address` = :address,
+                `public_comment` = :public_comment,
+                `private_comment` = :private_comment,
                 `goto` = :goto,
                 `goto` = :goto,
                 `active`= :active
                 `active`= :active
                   WHERE `id` = :id");
                   WHERE `id` = :id");
               $stmt->execute(array(
               $stmt->execute(array(
                 ':address' => $address,
                 ':address' => $address,
+                ':public_comment' => $public_comment,
+                ':private_comment' => $private_comment,
                 ':goto' => $goto,
                 ':goto' => $goto,
                 ':active' => $active,
                 ':active' => $active,
                 ':id' => $id
                 ':id' => $id
@@ -2367,6 +2448,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             'tls_enforce_out' => $attrs['tls_enforce_out']
             'tls_enforce_out' => $attrs['tls_enforce_out']
           );
           );
         break;
         break;
+        case 'quarantine_notification':
+          $attrs = 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'];
+          }
+          $stmt = $pdo->prepare("SELECT `attributes` FROM `mailbox` WHERE `username` = :username");
+          $stmt->execute(array(':username' => $_data));
+          $attrs = $stmt->fetch(PDO::FETCH_ASSOC);
+          $attrs = json_decode($attrs['attributes'], true);
+          return $attrs['quarantine_notification'];
+        break;
         case 'filters':
         case 'filters':
           $filters = array();
           $filters = array();
           if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
           if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
@@ -2699,6 +2796,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             `domain`,
             `domain`,
             `goto`,
             `goto`,
             `address`,
             `address`,
+            `public_comment`,
+            `private_comment`,
             `active` as `active_int`,
             `active` as `active_int`,
             CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
             CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
             `created`,
             `created`,
@@ -2722,6 +2821,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           }
           $aliasdata['id'] = $row['id'];
           $aliasdata['id'] = $row['id'];
           $aliasdata['domain'] = $row['domain'];
           $aliasdata['domain'] = $row['domain'];
+          $aliasdata['public_comment'] = $row['public_comment'];
+          $aliasdata['private_comment'] = $row['private_comment'];
+          $aliasdata['domain'] = $row['domain'];
           $aliasdata['goto'] = $row['goto'];
           $aliasdata['goto'] = $row['goto'];
           $aliasdata['address'] = $row['address'];
           $aliasdata['address'] = $row['address'];
           (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
           (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;

+ 15 - 3
data/web/inc/functions.quarantine.inc.php

@@ -81,12 +81,18 @@ function quarantine($_action, $_data = null) {
           $release_format = 'raw';
           $release_format = 'raw';
         }
         }
         $max_size = $_data['max_size'];
         $max_size = $_data['max_size'];
+        $subject = $_data['subject'];
+        $sender = $_data['sender'];
+        $html = $_data['html'];
         $exclude_domains = (array)$_data['exclude_domains'];
         $exclude_domains = (array)$_data['exclude_domains'];
         try {
         try {
           $redis->Set('Q_RETENTION_SIZE', intval($retention_size));
           $redis->Set('Q_RETENTION_SIZE', intval($retention_size));
           $redis->Set('Q_MAX_SIZE', intval($max_size));
           $redis->Set('Q_MAX_SIZE', intval($max_size));
           $redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains));
           $redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains));
           $redis->Set('Q_RELEASE_FORMAT', $release_format);
           $redis->Set('Q_RELEASE_FORMAT', $release_format);
+          $redis->Set('Q_SENDER', $sender);
+          $redis->Set('Q_SUBJECT', $subject);
+          $redis->Set('Q_HTML', $html);
         }
         }
         catch (RedisException $e) {
         catch (RedisException $e) {
           $_SESSION['return'][] = array(
           $_SESSION['return'][] = array(
@@ -393,7 +399,7 @@ function quarantine($_action, $_data = null) {
     break;
     break;
     case 'get':
     case 'get':
       if ($_SESSION['mailcow_cc_role'] == "user") {
       if ($_SESSION['mailcow_cc_role'] == "user") {
-        $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` = :mbox');
+        $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` = :mbox');
         $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username']));
         $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username']));
         $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
         $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
         while($row = array_shift($rows)) {
         while($row = array_shift($rows)) {
@@ -401,7 +407,7 @@ function quarantine($_action, $_data = null) {
         }
         }
       }
       }
       elseif ($_SESSION['mailcow_cc_role'] == "admin") {
       elseif ($_SESSION['mailcow_cc_role'] == "admin") {
-        $stmt = $pdo->query('SELECT `id`, `qid`, `subject`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine`');
+        $stmt = $pdo->query('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine`');
         $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
         $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
         while($row = array_shift($rows)) {
         while($row = array_shift($rows)) {
           $q_meta[] = $row;
           $q_meta[] = $row;
@@ -410,7 +416,7 @@ function quarantine($_action, $_data = null) {
       else {
       else {
         $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
         $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
         foreach ($domains as $domain) {
         foreach ($domains as $domain) {
-          $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` REGEXP :domain');
+          $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` REGEXP :domain');
           $stmt->execute(array(':domain' => '@' . $domain . '$'));
           $stmt->execute(array(':domain' => '@' . $domain . '$'));
           $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           while($row = array_shift($rows)) {
           while($row = array_shift($rows)) {
@@ -428,6 +434,12 @@ function quarantine($_action, $_data = null) {
         $settings['max_size'] = $redis->Get('Q_MAX_SIZE');
         $settings['max_size'] = $redis->Get('Q_MAX_SIZE');
         $settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE');
         $settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE');
         $settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT');
         $settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT');
+        $settings['subject'] = $redis->Get('Q_SUBJECT');
+        $settings['sender'] = $redis->Get('Q_SENDER');
+        $settings['html'] = htmlspecialchars($redis->Get('Q_HTML'));
+        if (empty($settings['html'])) {
+          $settings['html'] = htmlspecialchars(file_get_contents("/templates/quarantine.tpl"));
+        }
       }
       }
       catch (RedisException $e) {
       catch (RedisException $e) {
         $_SESSION['return'][] = array(
         $_SESSION['return'][] = array(

+ 7 - 1
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
   try {
     global $pdo;
     global $pdo;
 
 
-    $db_version = "17012019_0717";
+    $db_version = "27012019_1217";
 
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -137,6 +137,8 @@ function init_db_schema() {
           "domain" => "VARCHAR(255) NOT NULL",
           "domain" => "VARCHAR(255) NOT NULL",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
+          "private_comment" => "TEXT",
+          "public_comment" => "TEXT",
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
         ),
         ),
         "keys" => array(
         "keys" => array(
@@ -237,6 +239,7 @@ function init_db_schema() {
           "rcpt" => "VARCHAR(255)",
           "rcpt" => "VARCHAR(255)",
           "msg" => "LONGTEXT",
           "msg" => "LONGTEXT",
           "domain" => "VARCHAR(255)",
           "domain" => "VARCHAR(255)",
+          "notified" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
           "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
         ),
         ),
@@ -316,6 +319,8 @@ function init_db_schema() {
           "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'",
           ),
           ),
         "keys" => array(
         "keys" => array(
           "primary" => array(
           "primary" => array(
@@ -991,6 +996,7 @@ DELIMITER ;';
     $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_EXTRACT(`attributes`, '$.force_pw_update') IS NULL;");
     $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_EXTRACT(`attributes`, '$.force_pw_update') IS NULL;");
     $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_EXTRACT(`attributes`, '$.sogo_access') IS NULL;");
     $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_EXTRACT(`attributes`, '$.sogo_access') IS NULL;");
     $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_EXTRACT(`attributes`, '$.mailbox_format') IS NULL;");
     $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_EXTRACT(`attributes`, '$.mailbox_format') IS NULL;");
+    $stmt = $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_EXTRACT(`attributes`, '$.quarantine_notification') IS NULL;");
     foreach($tls_options as $tls_user => $tls_options) {
     foreach($tls_options as $tls_user => $tls_options) {
       $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in),
       $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in),
         `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out)
         `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out)

+ 6 - 0
data/web/inc/vars.inc.php

@@ -131,6 +131,9 @@ $DOCKER_TIMEOUT = 60;
 // Anonymize IPs logged via UI
 // Anonymize IPs logged via UI
 $ANONYMIZE_IPS = true;
 $ANONYMIZE_IPS = true;
 
 
+// MAILBOX_DEFAULT_ATTRIBUTES define default attributes for new mailboxes
+// These settings will not change existing mailboxes
+
 // Force incoming TLS for new mailboxes by default
 // Force incoming TLS for new mailboxes by default
 $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'] = false;
 $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'] = false;
 
 
@@ -143,6 +146,9 @@ $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
 // Force password change on next login (only allows login to mailcow UI)
 // Force password change on next login (only allows login to mailcow UI)
 $MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
 $MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
 
 
+// Send notification when quarantine is not empty (never, hourly, daily, weekly)
+$MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'] = 'never';
+
 // Default mailbox format, should not be changed unless you know exactly, what you do, keep the trailing ":"
 // Default mailbox format, should not be changed unless you know exactly, what you do, keep the trailing ":"
 // Check dovecot.conf for further changes (e.g. shared namespace)
 // Check dovecot.conf for further changes (e.g. shared namespace)
 $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';
 $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';

+ 6 - 6
data/web/js/admin.js

@@ -54,7 +54,7 @@ jQuery(function($){
   function draw_domain_admins() {
   function draw_domain_admins() {
     ft_domainadmins = FooTable.init('#domainadminstable', {
     ft_domainadmins = FooTable.init('#domainadminstable', {
       "columns": [
       "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","title":lang.username,"style":{"width":"250px"}},
         {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}},
         {"name":"selected_domains","title":lang.admin_domains,"breakpoints":"xs sm"},
         {"name":"selected_domains","title":lang.admin_domains,"breakpoints":"xs sm"},
         {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}},
         {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}},
@@ -82,7 +82,7 @@ jQuery(function($){
   function draw_admins() {
   function draw_admins() {
     ft_admins = FooTable.init('#adminstable', {
     ft_admins = FooTable.init('#adminstable', {
       "columns": [
       "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":"usr","title":lang.username,"style":{"width":"250px"}},
         {"sorted": true,"name":"usr","title":lang.username,"style":{"width":"250px"}},
         {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}},
         {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}},
         {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
         {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
@@ -109,7 +109,7 @@ jQuery(function($){
   function draw_fwd_hosts() {
   function draw_fwd_hosts() {
     ft_forwardinghoststable = FooTable.init('#forwardinghoststable', {
     ft_forwardinghoststable = FooTable.init('#forwardinghoststable', {
       "columns": [
       "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"},
         {"name":"host","type":"text","title":lang.host,"style":{"width":"250px"}},
         {"name":"host","type":"text","title":lang.host,"style":{"width":"250px"}},
         {"name":"source","title":lang.source,"breakpoints":"xs sm"},
         {"name":"source","title":lang.source,"breakpoints":"xs sm"},
         {"name":"keep_spam","title":lang.spamfilter, "type": "text","style":{"maxWidth":"80px","width":"80px"}},
         {"name":"keep_spam","title":lang.spamfilter, "type": "text","style":{"maxWidth":"80px","width":"80px"}},
@@ -134,7 +134,7 @@ jQuery(function($){
   function draw_relayhosts() {
   function draw_relayhosts() {
     ft_relayhoststable = FooTable.init('#relayhoststable', {
     ft_relayhoststable = FooTable.init('#relayhoststable', {
       "columns": [
       "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"},
         {"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
         {"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
         {"name":"hostname","type":"text","title":lang.host,"style":{"width":"250px"}},
         {"name":"hostname","type":"text","title":lang.host,"style":{"width":"250px"}},
         {"name":"username","title":lang.username,"breakpoints":"xs sm"},
         {"name":"username","title":lang.username,"breakpoints":"xs sm"},
@@ -161,7 +161,7 @@ jQuery(function($){
   function draw_transport_maps() {
   function draw_transport_maps() {
     ft_relayhoststable = FooTable.init('#transportstable', {
     ft_relayhoststable = FooTable.init('#transportstable', {
       "columns": [
       "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"},
         {"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
         {"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
         {"name":"destination","type":"text","title":lang.destination,"style":{"width":"250px"}},
         {"name":"destination","type":"text","title":lang.destination,"style":{"width":"250px"}},
         {"name":"nexthop","type":"text","title":lang.nexthop,"style":{"width":"250px"}},
         {"name":"nexthop","type":"text","title":lang.nexthop,"style":{"width":"250px"}},
@@ -188,7 +188,7 @@ jQuery(function($){
   function draw_queue() {
   function draw_queue() {
     ft_queuetable = FooTable.init('#queuetable', {
     ft_queuetable = FooTable.init('#queuetable', {
       "columns": [
       "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"},
         {"name":"queue_id","type":"text","title":"QID","style":{"width":"50px"}},
         {"name":"queue_id","type":"text","title":"QID","style":{"width":"50px"}},
         {"name":"queue_name","type":"text","title":"Queue","style":{"width":"120px"}},
         {"name":"queue_name","type":"text","title":"Queue","style":{"width":"120px"}},
         {"name":"arrival_time","sorted": true,"direction": "DESC","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.arrival_time,"style":{"width":"170px"}},
         {"name":"arrival_time","sorted": true,"direction": "DESC","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.arrival_time,"style":{"width":"170px"}},

+ 1 - 2
data/web/js/api.js

@@ -193,7 +193,6 @@ $(document).ready(function() {
         }
         }
         if ($(this).attr("max")) {
         if ($(this).attr("max")) {
           if (Number($(this).val()) > Number($(this).attr("max"))) {
           if (Number($(this).val()) > Number($(this).attr("max"))) {
-            alert($(this).attr("max"))
             invalid = true;
             invalid = true;
             $(this).addClass('inputMissingAttr');
             $(this).addClass('inputMissingAttr');
           } else {
           } else {
@@ -314,4 +313,4 @@ $(document).ready(function() {
         $('#ConfirmDeleteModal').modal('hide');
         $('#ConfirmDeleteModal').modal('hide');
       });
       });
   });
   });
-});
+});

+ 10 - 3
data/web/js/debug.js

@@ -381,11 +381,13 @@ jQuery(function($){
         function drawChart() {
         function drawChart() {
 
 
           var data = google.visualization.arrayToDataTable(graphdata);
           var data = google.visualization.arrayToDataTable(graphdata);
-
+          var body_font_color = $('body').css("color");
           var options = {
           var options = {
             is3D: true,
             is3D: true,
             sliceVisibilityThreshold: 0,
             sliceVisibilityThreshold: 0,
             pieSliceText: 'percentage',
             pieSliceText: 'percentage',
+            backgroundColor: { fill:'transparent' },
+            legend: {textStyle: {color: body_font_color}},
             chartArea: {
             chartArea: {
               left: 0,
               left: 0,
               right: 0,
               right: 0,
@@ -416,7 +418,7 @@ jQuery(function($){
         {"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":"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": "ip","title": "IP address","breakpoints": "all","style": {"minWidth": 88}},
         {"name": "sender_mime","title": "From","breakpoints": "xs sm md","style": {"minWidth": 100}},
         {"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": "rcpt","title": "To","breakpoints": "xs sm md","style": {"minWidth": 100}},
         {"name": "subject","title": "Subject","breakpoints": "all","style": {"word-break": "break-all","minWidth": 150}},
         {"name": "subject","title": "Subject","breakpoints": "all","style": {"word-break": "break-all","minWidth": 150}},
         {"name": "action","title": "Action","style": {"minwidth": 82}},
         {"name": "action","title": "Action","style": {"minwidth": 82}},
         {"name": "score","title": "Score","style": {"maxWidth": 110},},
         {"name": "score","title": "Score","style": {"maxWidth": 110},},
@@ -460,7 +462,12 @@ jQuery(function($){
   function process_table_data(data, table) {
   function process_table_data(data, table) {
     if (table == 'rspamd_history') {
     if (table == 'rspamd_history') {
     $.each(data, function (i, item) {
     $.each(data, function (i, item) {
-      item.rcpt_mime = item.rcpt_mime.join(",&#8203;");
+      if (item.rcpt_mime != "") {
+        item.rcpt = item.rcpt_mime.join(", ");
+      }
+      else {
+        item.rcpt = item.rcpt_smtp.join(", ");
+      }
       Object.keys(item.symbols).map(function(key) {
       Object.keys(item.symbols).map(function(key) {
         var sym = item.symbols[key];
         var sym = item.symbols[key];
         if (sym.score <= 0) {
         if (sym.score <= 0) {

+ 0 - 2
data/web/js/edit.js

@@ -22,8 +22,6 @@ $(document).ready(function() {
     $('#textarea_alias_goto').prop('disabled', true);
     $('#textarea_alias_goto').prop('disabled', true);
   }
   }
 
 
-  $("#script_data").numberedtextarea({allowTabChar: true});
-
   $("#mailbox-password-warning-close").click(function( event ) {
   $("#mailbox-password-warning-close").click(function( event ) {
     $('#mailbox-passwd-hidden-info').addClass('hidden');
     $('#mailbox-passwd-hidden-info').addClass('hidden');
     $('#mailbox-passwd-form-groups').removeClass('hidden');
     $('#mailbox-passwd-form-groups').removeClass('hidden');

+ 4 - 2
data/web/js/mailbox.js

@@ -141,8 +141,6 @@ $(document).ready(function() {
     var sieveScript = $(e.relatedTarget).data('sieve-script');
     var sieveScript = $(e.relatedTarget).data('sieve-script');
     $(e.currentTarget).find('#sieveDataText').html('<pre style="font-size:14px;line-height:1.1">' + sieveScript + '</pre>');
     $(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
   // Disable submit button on script change
 	$('#script_data').on('keyup', function() {
 	$('#script_data').on('keyup', function() {
     $('#add_filter_btns > #add_sieve_script').attr({"disabled": true});
     $('#add_filter_btns > #add_sieve_script').attr({"disabled": true});
@@ -712,6 +710,8 @@ jQuery(function($){
         {"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}},
         {"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}},
         {"name":"goto","title":lang.target_address},
         {"name":"goto","title":lang.target_address},
         {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
         {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
+        {"name":"public_comment","title":lang.public_comment,"breakpoints":"all"},
+        {"name":"private_comment","title":lang.private_comment,"breakpoints":"all"},
         {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
         {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
       ],
       ],
@@ -731,6 +731,8 @@ jQuery(function($){
               '</div>';
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
             item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
             item.goto = escapeHtml(item.goto.replace(/,/g, " "));
             item.goto = escapeHtml(item.goto.replace(/,/g, " "));
+            item.public_comment = escapeHtml(item.public_comment);
+            item.private_comment = escapeHtml(item.private_comment);
             if (item.is_catch_all == 1) {
             if (item.is_catch_all == 1) {
               item.address = '<div class="label label-default">Catch-All</div> ' + escapeHtml(item.address);
               item.address = '<div class="label label-default">Catch-All</div> ' + escapeHtml(item.address);
             }
             }

+ 2 - 1
data/web/js/mailcow.js

@@ -161,7 +161,8 @@ $(document).ready(function() {
       $(document).on("keydown", disableF5);
       $(document).on("keydown", disableF5);
     }
     }
   });
   });
-
+  // Textarea line numbers
+  $(".textarea-code").numberedtextarea({allowTabChar: true});
   // trigger container restart
   // trigger container restart
   $('#RestartContainer').on('show.bs.modal', function(e) {
   $('#RestartContainer').on('show.bs.modal', function(e) {
     var container = $(e.relatedTarget).data('container');
     var container = $(e.relatedTarget).data('container');

+ 22 - 10
data/web/js/quarantine.js

@@ -1,22 +1,23 @@
 // Base64 functions
 // 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}};
 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($){
 jQuery(function($){
+  acl_data = JSON.parse(acl);
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
   var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
   var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
   function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
   function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
   function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
   function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
-
   function draw_quarantine_table() {
   function draw_quarantine_table() {
     ft_quarantinetable = FooTable.init('#quarantinetable', {
     ft_quarantinetable = FooTable.init('#quarantinetable', {
       "columns": [
       "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"},
         {"name":"id","type":"ID","filterable": false,"sorted": true,"direction":"DESC","title":"ID","style":{"width":"50px"}},
         {"name":"id","type":"ID","filterable": false,"sorted": true,"direction":"DESC","title":"ID","style":{"width":"50px"}},
-        {"name":"qid","type":"text","title":lang.qid,"style":{"width":"125px"}},
-        {"name":"sender","style":{"word-break":"break-all"},"title":lang.sender,"breakpoints":"xs sm"},
-        {"name":"rcpt","title":lang.rcpt, "type": "text"},
-        {"name":"subject","title":"Subject", "type": "text"},
+        {"name":"qid","breakpoints":"all","type":"text","title":lang.qid,"style":{"width":"125px"}},
+        {"name":"sender","title":lang.sender},
+        {"name":"rcpt","title":lang.rcpt, "breakpoints":"xs sm md", "type": "text"},
+        {"name":"virus","title":lang.danger, "type": "text"},
+        {"name":"subject","title":lang.subj, "type": "text"},
         {"name":"created","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.received,"style":{"width":"170px"}},
         {"name":"created","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.received,"style":{"width":"170px"}},
-        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right"},"style":{"width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right"},"style":{"width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
       ],
       ],
       "rows": $.ajax({
       "rows": $.ajax({
         dataType: 'json',
         dataType: 'json',
@@ -28,15 +29,26 @@ jQuery(function($){
         success: function (data) {
         success: function (data) {
           $.each(data, function (i, item) {
           $.each(data, function (i, item) {
             if (item.subject === null) {
             if (item.subject === null) {
-              item.subject = '<i>no preview</i>';
-            }
-            else {
+              item.subject = '';
+            } else {
               item.subject = escapeHtml(item.subject);
               item.subject = escapeHtml(item.subject);
             }
             }
+            if (item.virus_flag > 0) {
+              item.virus = '<span class="dot-danger"></span>';
+            } else {
+              item.virus = '<span class="dot-neutral"></span>';
+            }
+            if (acl_data.login_as === 1) {
             item.action = '<div class="btn-group">' +
             item.action = '<div class="btn-group">' +
               '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><span class="glyphicon glyphicon-modal-window"></span> ' + lang.show_item + '</a>' +
               '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><span class="glyphicon glyphicon-modal-window"></span> ' + lang.show_item + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
               '</div>';
+            }
+            else {
+            item.action = '<div class="btn-group">' +
+              '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><span class="glyphicon glyphicon-modal-window"></span> ' + lang.show_item + '</a>' +
+              '</div>';
+            }
             item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
             item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
           });
           });
         }
         }

+ 3 - 0
data/web/json_api.php

@@ -1152,6 +1152,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "tls_policy":
           case "tls_policy":
             process_edit_return(mailbox('edit', 'tls_policy', array_merge(array('username' => $items), $attr)));
             process_edit_return(mailbox('edit', 'tls_policy', array_merge(array('username' => $items), $attr)));
           break;
           break;
+          case "quarantine_notification":
+            process_edit_return(mailbox('edit', 'quarantine_notification', array_merge(array('username' => $items), $attr)));
+          break;
           case "qitem":
           case "qitem":
             process_edit_return(quarantine('edit', array_merge(array('id' => $items), $attr)));
             process_edit_return(quarantine('edit', array_merge(array('id' => $items), $attr)));
           break;
           break;

+ 2 - 2
data/web/lang/lang.cs.php

@@ -652,8 +652,8 @@ $lang['admin']['no_active_bans'] = "Žádná aktivní blokování";
 $lang['admin']['quarantine'] = "Karanténa";
 $lang['admin']['quarantine'] = "Karanténa";
 $lang['admin']['quarantine_retention_size'] = "Počet zadržených zpráv na poštovní schránku<br />0 znamená <b>neaktivní</b>!";
 $lang['admin']['quarantine_retention_size'] = "Počet zadržených zpráv na poštovní schránku<br />0 znamená <b>neaktivní</b>!";
 $lang['admin']['quarantine_max_size'] = "Maximální velikost v MiB (větší prvky budou smazány)<br />0 <b>neznamená</b> neomezeno!";
 $lang['admin']['quarantine_max_size'] = "Maximální velikost v MiB (větší prvky budou smazány)<br />0 <b>neznamená</b> neomezeno!";
-$lang['admin']['quarantine_exclude_domains'] = "Vyloučené domény a doménové aliasy:";
-$lang['admin']['quarantine_release_format'] = "Formát propuštěných položek:";
+$lang['admin']['quarantine_exclude_domains'] = "Vyloučené domény a doménové aliasy";
+$lang['admin']['quarantine_release_format'] = "Formát propuštěných položek";
 $lang['admin']['quarantine_release_format_raw'] = "Nezměněný originál";
 $lang['admin']['quarantine_release_format_raw'] = "Nezměněný originál";
 $lang['admin']['quarantine_release_format_att'] = "Jako příloha";
 $lang['admin']['quarantine_release_format_att'] = "Jako příloha";
 
 

+ 18 - 3
data/web/lang/lang.de.php

@@ -378,6 +378,14 @@ $lang['edit']['dont_check_sender_acl'] = 'Absender für Domain %s u. Alias-Dom.
 $lang['edit']['multiple_bookings'] = 'Mehrfaches Buchen';
 $lang['edit']['multiple_bookings'] = 'Mehrfaches Buchen';
 $lang['edit']['kind'] = 'Art';
 $lang['edit']['kind'] = 'Art';
 $lang['edit']['resource'] = 'Ressource';
 $lang['edit']['resource'] = 'Ressource';
+$lang['edit']['public_comment'] = 'Öffentlicher Kommentar';
+$lang['mailbox']['public_comment'] = 'Öffentlicher Kommentar';
+$lang['edit']['private_comment'] = 'Privater Kommentar';
+$lang['mailbox']['private_comment'] = 'Privater Kommentar';
+$lang['edit']['comment_info'] = 'Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.';
+$lang['add']['public_comment'] = 'Öffentlicher Kommentar';
+$lang['add']['private_comment'] = 'Privater Kommentar';
+$lang['add']['comment_info'] = 'Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.';
 
 
 $lang['acl']['spam_alias'] = 'Temporäre E-Mail Aliasse';
 $lang['acl']['spam_alias'] = 'Temporäre E-Mail Aliasse';
 $lang['acl']['tls_policy'] = 'Verschlüsselungsrichtlinie';
 $lang['acl']['tls_policy'] = 'Verschlüsselungsrichtlinie';
@@ -630,9 +638,15 @@ $lang['admin']['queue_unban'] = "Unban einreihen";
 $lang['admin']['no_active_bans'] = "Keine aktiven Bans";
 $lang['admin']['no_active_bans'] = "Keine aktiven Bans";
 
 
 $lang['admin']['quarantine'] = "Quarantäne";
 $lang['admin']['quarantine'] = "Quarantäne";
-$lang['admin']['quarantine_retention_size'] = "Rückhaltungen pro Mailbox<br />0 bedeutet <b>inaktiv</b>!";
-$lang['admin']['quarantine_max_size'] = "Maximale Größe in MiB (größere Elemente werden verworfen)<br />0 bedeutet <b>nicht</b> unlimitert!";
-$lang['admin']['quarantine_exclude_domains'] = "Domains und Alias-Domains ausschließen:";
+$lang['admin']['quarantine_retention_size'] = "Rückhaltungen pro Mailbox:<br><small>0 bedeutet <b>inaktiv</b>.</small>";
+$lang['admin']['quarantine_max_size'] = "Maximale Größe in MiB (größere Elemente werden verworfen):<br><small>0 bedeutet <b>nicht</b> unlimitert.</small>";
+$lang['admin']['quarantine_exclude_domains'] = "Domains und Alias-Domains ausschließen";
+$lang['admin']['quarantine_notification_sender'] = "Benachrichtigungs-E-Mail Absender";
+$lang['admin']['quarantine_notification_subject'] = "Benachrichtigungs-E-Mail Betreff";
+$lang['admin']['quarantine_notification_html'] = "Benachrichtigungs-E-Mail Inhalt:<br><small>Leer lassen, um Standard-Template wiederherzustellen.</small>";
+$lang['admin']['quarantine_release_format'] = "Format freigegebener Mails";
+$lang['admin']['quarantine_release_format_raw'] = "Unverändertes Original";
+$lang['admin']['quarantine_release_format_att'] = "Als Anhang";
 
 
 $lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt";
 $lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt";
 $lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt";
 $lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt";
@@ -676,6 +690,7 @@ $lang['edit']['spam_policy'] = "Hinzufügen und Entfernen von Einträgen in Whit
 $lang['edit']['spam_alias'] = "Anpassen temporärer Alias-Adressen";
 $lang['edit']['spam_alias'] = "Anpassen temporärer Alias-Adressen";
 
 
 $lang['danger']['img_tmp_missing'] = "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen";
 $lang['danger']['img_tmp_missing'] = "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen";
+$lang['danger']['comment_too_long'] = "Kommentarfeld darf maximal 160 Zeichen enthalten";
 $lang['danger']['img_invalid'] = "Grafik konnte nicht validiert werden";
 $lang['danger']['img_invalid'] = "Grafik konnte nicht validiert werden";
 $lang['danger']['invalid_mime_type'] = "Grafik konnte nicht validiert werden: Ungültiger MIME-Type";
 $lang['danger']['invalid_mime_type'] = "Grafik konnte nicht validiert werden: Ungültiger MIME-Type";
 $lang['success']['upload_success'] = "Datei wurde erfolgreich hochgeladen";
 $lang['success']['upload_success'] = "Datei wurde erfolgreich hochgeladen";

+ 16 - 6
data/web/lang/lang.en.php

@@ -392,7 +392,14 @@ $lang['edit']['multiple_bookings'] = 'Multiple bookings';
 $lang['edit']['kind'] = 'Kind';
 $lang['edit']['kind'] = 'Kind';
 $lang['edit']['resource'] = 'Resource';
 $lang['edit']['resource'] = 'Resource';
 $lang['edit']['relayhost'] = 'Sender-dependent transports';
 $lang['edit']['relayhost'] = 'Sender-dependent transports';
-
+$lang['edit']['public_comment'] = 'Public comment';
+$lang['mailbox']['public_comment'] = 'Public comment';
+$lang['edit']['private_comment'] = 'Private comment';
+$lang['mailbox']['private_comment'] = 'Private comment';
+$lang['edit']['comment_info'] = 'A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a users overview';
+$lang['add']['public_comment'] = 'Public comment';
+$lang['add']['private_comment'] = 'Private comment';
+$lang['add']['comment_info'] = 'A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a users overview';
 $lang['acl']['spam_alias'] = 'Temporary aliases';
 $lang['acl']['spam_alias'] = 'Temporary aliases';
 $lang['acl']['tls_policy'] = 'TLS policy';
 $lang['acl']['tls_policy'] = 'TLS policy';
 $lang['acl']['spam_score'] = 'Spam score';
 $lang['acl']['spam_score'] = 'Spam score';
@@ -666,13 +673,15 @@ $lang['admin']['queue_unban'] = "queue unban";
 $lang['admin']['no_active_bans'] = "No active bans";
 $lang['admin']['no_active_bans'] = "No active bans";
 
 
 $lang['admin']['quarantine'] = "Quarantine";
 $lang['admin']['quarantine'] = "Quarantine";
-$lang['admin']['quarantine_retention_size'] = "Retentions per mailbox<br />0 indicates <b>inactive</b>!";
-$lang['admin']['quarantine_max_size'] = "Maximum size in MiB (larger elements are discarded)<br />0 does <b>not</b> indicate unlimited!";
-$lang['admin']['quarantine_exclude_domains'] = "Exclude domains and alias-domains:";
-$lang['admin']['quarantine_release_format'] = "Format of released items:";
+$lang['admin']['quarantine_retention_size'] = "Retentions per mailbox:<br><small>0 indicates <b>inactive</b>.</small>";
+$lang['admin']['quarantine_max_size'] = "Maximum size in MiB (larger elements are discarded):<br><small>0 does <b>not</b> indicate unlimited.</small>";
+$lang['admin']['quarantine_exclude_domains'] = "Exclude domains and alias-domains";
+$lang['admin']['quarantine_release_format'] = "Format of released items";
 $lang['admin']['quarantine_release_format_raw'] = "Unmodified original";
 $lang['admin']['quarantine_release_format_raw'] = "Unmodified original";
 $lang['admin']['quarantine_release_format_att'] = "As attachment";
 $lang['admin']['quarantine_release_format_att'] = "As attachment";
-
+$lang['admin']['quarantine_notification_sender'] = "Notification email sender";
+$lang['admin']['quarantine_notification_subject'] = "Notification email subject";
+$lang['admin']['quarantine_notification_html'] = "Notification email template:<br><small>Leave empty to restore default template.</small>";
 $lang['admin']['ui_texts'] = "UI labels and texts";
 $lang['admin']['ui_texts'] = "UI labels and texts";
 $lang['admin']['help_text'] = "Override help text below login mask (HTML allowed)";
 $lang['admin']['help_text'] = "Override help text below login mask (HTML allowed)";
 $lang['admin']['title_name'] = '"mailcow UI" website title';
 $lang['admin']['title_name'] = '"mailcow UI" website title';
@@ -699,6 +708,7 @@ $lang['user']['spam_score_reset'] = "Reset to server default";
 $lang['edit']['spam_policy'] = "Add or remove items to white-/blacklist";
 $lang['edit']['spam_policy'] = "Add or remove items to white-/blacklist";
 $lang['edit']['spam_alias'] = "Create or change time limited alias addresses";
 $lang['edit']['spam_alias'] = "Create or change time limited alias addresses";
 
 
+$lang['danger']['comment_too_long'] = "Comment too long, max 160 chars allowed";
 $lang['danger']['img_tmp_missing'] = "Cannot validate image file: Temporary file not found";
 $lang['danger']['img_tmp_missing'] = "Cannot validate image file: Temporary file not found";
 $lang['danger']['img_invalid'] = "Cannot validate image file";
 $lang['danger']['img_invalid'] = "Cannot validate image file";
 $lang['danger']['invalid_mime_type'] = "Invalid mime type";
 $lang['danger']['invalid_mime_type'] = "Invalid mime type";

+ 2 - 2
data/web/lang/lang.nl.php

@@ -638,8 +638,8 @@ $lang['admin']['no_active_bans'] = "Geen actieve verbanningen";
 $lang['admin']['quarantine'] = "Quarantaine";
 $lang['admin']['quarantine'] = "Quarantaine";
 $lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak<br />Gebruik 0 om deze functionaliteit <b>uit te zetten</b>.";
 $lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak<br />Gebruik 0 om deze functionaliteit <b>uit te zetten</b>.";
 $lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd)<br />0 betekent <b>niet</b> onbeperkt!";
 $lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd)<br />0 betekent <b>niet</b> onbeperkt!";
-$lang['admin']['quarantine_exclude_domains'] = "Sluit domeinen en aliasdomeinen uit:";
-$lang['admin']['quarantine_release_format'] = "Vrijgegeven items worden verstuurd als:";
+$lang['admin']['quarantine_exclude_domains'] = "Sluit domeinen en aliasdomeinen uit";
+$lang['admin']['quarantine_release_format'] = "Vrijgegeven items worden verstuurd als";
 $lang['admin']['quarantine_release_format_raw'] = "Origineel";
 $lang['admin']['quarantine_release_format_raw'] = "Origineel";
 $lang['admin']['quarantine_release_format_att'] = "Bijlage";
 $lang['admin']['quarantine_release_format_att'] = "Bijlage";
 
 

+ 1 - 0
data/web/mailbox.php

@@ -1,5 +1,6 @@
 <?php
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
 require_once $_SERVER['DOCUMENT_ROOT'] .  '/inc/header.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] .  '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];

+ 1 - 1
data/web/modals/mailbox.php

@@ -572,7 +572,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="script_data">Script:</label>
 						<label class="control-label col-sm-2" for="script_data">Script:</label>
 						<div class="col-sm-10">
 						<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>
+							<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" id="script_data" name="script_data" required></textarea>
 						</div>
 						</div>
 					</div>
 					</div>
 					<div class="form-group">
 					<div class="form-group">

+ 7 - 1
data/web/modals/quarantine.php

@@ -25,11 +25,17 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <label for="qid_detail_text_from_html"><h4><?=$lang['quarantine']['text_from_html_content'];?>:</h4></label>
           <label for="qid_detail_text_from_html"><h4><?=$lang['quarantine']['text_from_html_content'];?>:</h4></label>
           <pre id="qid_detail_text_from_html"></pre>
           <pre id="qid_detail_text_from_html"></pre>
         </div>
         </div>
+        <?php
+        if ($_SESSION['acl']['quarantine_attachments'] == 1):
+        ?>
         <div class="form-group">
         <div class="form-group">
           <label for="qid_detail_atts"><h4><?=$lang['quarantine']['atts'];?>:</h4></label>
           <label for="qid_detail_atts"><h4><?=$lang['quarantine']['atts'];?>:</h4></label>
           <div id="qid_detail_atts">-</div>
           <div id="qid_detail_atts">-</div>
         </div>
         </div>
-        <div class="btn-group" data-acl="<?=$_SESSION['acl']['quarantine'];?>">
+        <?php
+        endif;
+        ?>
+        <div class="btn-group dropup" data-acl="<?=$_SESSION['acl']['quarantine'];?>">
           <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['quarantine']['quick_actions'];?> <span class="caret"></span></a>
           <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['quarantine']['quick_actions'];?> <span class="caret"></span></a>
           <ul class="dropdown-menu">
           <ul class="dropdown-menu">
             <li><a data-action="edit_selected" data-id="qitems_single" data-item="" data-api-url='edit/qitem' data-api-attr='{"action":"release"}' href="#"><?=$lang['quarantine']['release'];?></a></li>
             <li><a data-action="edit_selected" data-id="qitems_single" data-item="" data-api-url='edit/qitem' data-api-attr='{"action":"release"}' href="#"><?=$lang['quarantine']['release'];?></a></li>

+ 1 - 1
data/web/modals/user.php

@@ -168,7 +168,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     <div class="modal-content">
     <div class="modal-content">
       <div class="modal-header"><h4 class="modal-title">Log</h4></div>
       <div class="modal-header"><h4 class="modal-title">Log</h4></div>
       <div class="modal-body">
       <div class="modal-body">
-        <textarea class="form-control" rows="20" id="logText" spellcheck="false"></textarea>
+        <textarea class="form-control textarea-code" rows="20" id="logText" spellcheck="false"></textarea>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>

+ 7 - 1
data/web/quarantine.php

@@ -18,7 +18,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
         <?php
         <?php
         if (empty(quarantine('settings')['retention_size']) || empty(quarantine('settings')['max_size'])):
         if (empty(quarantine('settings')['retention_size']) || empty(quarantine('settings')['max_size'])):
         ?>
         ?>
-        <span class="help-block"><span class="glyphicon glyphicon-remove text-danger" style="font-size:10px"></span> <b><?=$lang['quarantine']['disabled_by_config'];?></b></span>
+        <div class="panel-body"><div class="alert alert-info"><?=$lang['quarantine']['disabled_by_config'];?></div></div>
         <?php
         <?php
         endif;
         endif;
         ?>
         ?>
@@ -39,6 +39,11 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
             </ul>
             </ul>
           </div>
           </div>
         </div>
         </div>
+        <hr>
+        <div class="panel-body help-block">
+        <p><span class="dot-danger"></span> <?=$lang['quarantine']['high_danger'];?></p>
+        <p><span class="dot-neutral"></span> <?=$lang['quarantine']['neutral_danger'];?></p>
+        </div>
       </div>
       </div>
     </div> <!-- /col-md-12 -->
     </div> <!-- /col-md-12 -->
   </div> <!-- /row -->
   </div> <!-- /row -->
@@ -49,6 +54,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/quarantine.php';
 <script type='text/javascript'>
 <script type='text/javascript'>
 <?php
 <?php
 $lang_mailbox = json_encode($lang['quarantine']);
 $lang_mailbox = json_encode($lang['quarantine']);
+echo "var acl = '". json_encode($_SESSION['acl']) . "';\n";
 echo "var lang = ". $lang_mailbox . ";\n";
 echo "var lang = ". $lang_mailbox . ";\n";
 echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
 echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
 $role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
 $role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';

+ 60 - 3
data/web/user.php

@@ -125,7 +125,18 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
       <p class="small"><?=$lang['user']['direct_aliases_desc'];?></p>
       <p class="small"><?=$lang['user']['direct_aliases_desc'];?></p>
     </div>
     </div>
     <div class="col-md-9 col-xs-7">
     <div class="col-md-9 col-xs-7">
-    <p><?=$user_get_alias_details['direct_aliases'];?></p>
+    <?php
+    if ($user_get_alias_details['direct_aliases'] === false) {
+      echo '&#10008;';
+    }
+    else {
+      foreach (array_filter($user_get_alias_details['direct_aliases']) as $direct_alias => $direct_alias_meta) {
+        (!empty($direct_alias_meta['public_comment'])) ?
+          printf('%s <small>(%s)</small><br>', $direct_alias, $direct_alias_meta['public_comment']) :
+          printf('%s<br>', $direct_alias);
+      }
+    }
+    ?>
     </div>
     </div>
   </div>
   </div>
   <div class="row">
   <div class="row">
@@ -133,7 +144,18 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
       <p class="small"><?=$lang['user']['shared_aliases_desc'];?></p>
       <p class="small"><?=$lang['user']['shared_aliases_desc'];?></p>
     </div>
     </div>
     <div class="col-md-9 col-xs-7">
     <div class="col-md-9 col-xs-7">
-    <p><?=$user_get_alias_details['shared_aliases'];?></p>
+    <?php
+    if ($user_get_alias_details['shared_aliases'] === false) {
+      echo '&#10008;';
+    }
+    else {
+      foreach (array_filter($user_get_alias_details['shared_aliases']) as $shared_alias => $shared_alias_meta) {
+        (!empty($shared_alias_meta['public_comment'])) ?
+          printf('%s <small>(%s)</small><br>', $shared_alias, $shared_alias_meta['public_comment']) :
+          printf('%s<br>', $shared_alias);
+      }
+    }
+    ?>
     </div>
     </div>
   </div>
   </div>
   <hr>
   <hr>
@@ -223,7 +245,42 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
     <p class="help-block"><?=$lang['user']['tls_policy_warning'];?></p>
     <p class="help-block"><?=$lang['user']['tls_policy_warning'];?></p>
     </div>
     </div>
   </div>
   </div>
-
+  <?php
+  // Show quarantine_notification options
+  $quarantine_notification = mailbox('get', 'quarantine_notification', $username);
+  ?>
+  <div class="row">
+    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['quarantine_notification'];?>:</div>
+    <div class="col-md-9 col-xs-7">
+    <div class="btn-group" data-acl="<?=$_SESSION['acl']['quarantine_notification'];?>">
+      <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "never") ? "active" : null;?>"
+        data-action="edit_selected"
+        data-item="<?= htmlentities($username); ?>"
+        data-id="quarantine_notification"
+        data-api-url='edit/quarantine_notification'
+        data-api-attr='{"quarantine_notification":"never"}'><?=$lang['user']['never'];?></button>
+      <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "hourly") ? "active" : null;?>"
+        data-action="edit_selected"
+        data-item="<?= htmlentities($username); ?>"
+        data-id="quarantine_notification"
+        data-api-url='edit/quarantine_notification'
+        data-api-attr='{"quarantine_notification":"hourly"}'><?=$lang['user']['hourly'];?></button>
+      <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "daily") ? "active" : null;?>"
+        data-action="edit_selected"
+        data-item="<?= htmlentities($username); ?>"
+        data-id="quarantine_notification"
+        data-api-url='edit/quarantine_notification'
+        data-api-attr='{"quarantine_notification":"daily"}'><?=$lang['user']['daily'];?></button>
+      <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "weekly") ? "active" : null;?>"
+        data-action="edit_selected"
+        data-item="<?= htmlentities($username); ?>"
+        data-id="quarantine_notification"
+        data-api-url='edit/quarantine_notification'
+        data-api-attr='{"quarantine_notification":"weekly"}'><?=$lang['user']['weekly'];?></button>
+    </div>
+    <p class="help-block"><?=$lang['user']['quarantine_notification_info'];?></p>
+    </div>
+  </div>
   <hr>
   <hr>
 
 
   <div class="row">
   <div class="row">