Browse Source

[Web] Use INTL_IDNA_VARIANT_UTS46 in idn_to_ascii (thanks to @Knight1 !)
[Web] Some PHP fixes (warnings, notices)
[Web] Add quota notification tools

andryyy 6 years ago
parent
commit
1e764009bf

+ 116 - 54
data/web/admin.php

@@ -76,8 +76,8 @@ $tfa_data = get_tfa();
             </select>
           </div>
         </div>
-        <legend data-target="#api" style="margin-top:40px;cursor:pointer" id="api_legend" unselectable="on" data-toggle="collapse">
-          <span id="api_arrow" style="font-size:12px" class="rotate glyphicon glyphicon-menu-down"></span> API (experimental, work in progress)
+        <legend data-target="#api" style="margin-top:40px;cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> API (experimental, work in progress)
         </legend>
         <?php
         $api = admin_api('get');
@@ -283,6 +283,7 @@ $tfa_data = get_tfa();
         <a href="#fwdhosts" class="list-group-item"><?=$lang['admin']['forwarding_hosts'];?></a>
         <a href="#f2bparams" class="list-group-item"><?=$lang['admin']['f2b_parameters'];?></a>
         <a href="#quarantine" class="list-group-item"><?=$lang['admin']['quarantine'];?></a>
+        <a href="#quota" class="list-group-item">Quota notifications</a>
         <a href="#rsettings" class="list-group-item">Rspamd settings map</a>
         <a href="#customize" class="list-group-item"><?=$lang['admin']['customize'];?></a>
         <a href="#top" class="list-group-item" style="border-top:1px dashed #dadada">↸ <?=$lang['admin']['to_top'];?></a>
@@ -415,8 +416,8 @@ $tfa_data = get_tfa();
           <button class="btn btn-sm btn-default" data-action="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
         </form>
 
-        <legend data-target="#import_dkim" style="margin-top:40px;cursor:pointer" id="import_dkim_legend" unselectable="on" data-toggle="collapse">
-          <span id="import_dkim_arrow" style="font-size:12px" class="rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['import_private_key'];?>
+        <legend data-target="#import_dkim" style="margin-top:40px;cursor:pointer" class="arrow-toggle"" unselectable="on" data-toggle="collapse">
+          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['import_private_key'];?>
         </legend>
         <div id="import_dkim" class="collapse">
         <form class="form" data-id="dkim_import" role="form" method="post">
@@ -436,8 +437,8 @@ $tfa_data = get_tfa();
         </form>
         </div>
 
-        <legend data-target="#duplicate_dkim" style="margin-top:40px;cursor:pointer" id="duplicate_dkim_legend" unselectable="on" data-toggle="collapse">
-          <span id="duplicate_dkim_arrow" style="font-size:12px" class="rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['duplicate_dkim'];?>
+        <legend data-target="#duplicate_dkim" style="margin-top:40px;cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['duplicate_dkim'];?>
         </legend>
         <div id="duplicate_dkim" class="collapse">
         <form class="form-horizontal" data-id="dkim_duplicate" role="form" method="post">
@@ -579,32 +580,36 @@ $tfa_data = get_tfa();
         <i><?=$lang['admin']['no_active_bans'];?></i>
         <?php
         endif;
-        foreach ($f2b_data['active_bans'] as $active_bans):
-        ?>
-        <p><span class="label label-info" style="padding:4px;font-size:85%;"><span class="glyphicon glyphicon-filter"></span> <?=$active_bans['network'];?> (<?=$active_bans['banned_until'];?>) - 
-          <?php
-          if ($active_bans['queued_for_unban'] == 0):
+        if (!empty($f2b_data['active_bans'])):
+          foreach ($f2b_data['active_bans'] as $active_bans):
           ?>
-          <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[<?=$lang['admin']['queue_unban'];?>]</a>
-          <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
-          <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist]</a>
+          <p><span class="label label-info" style="padding:4px;font-size:85%;"><span class="glyphicon glyphicon-filter"></span> <?=$active_bans['network'];?> (<?=$active_bans['banned_until'];?>) - 
+            <?php
+            if ($active_bans['queued_for_unban'] == 0):
+            ?>
+            <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[<?=$lang['admin']['queue_unban'];?>]</a>
+            <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
+            <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist]</a>
+            <?php
+            else:
+            ?>
+            <i><?=$lang['admin']['unban_pending'];?></i>
+            <?php
+            endif;
+            ?>
+          </span></p>
           <?php
-          else:
+          endforeach;
+        endif;
+        if (!empty($f2b_data['perm_bans'])):
+          foreach ($f2b_data['perm_bans'] as $perm_bans):
           ?>
-          <i><?=$lang['admin']['unban_pending'];?></i>
+          <p>
+          <span class="label label-danger" style="padding:4px;font-size:85%;"><span class="glyphicon glyphicon-filter"></span> <?=$perm_bans?></span>
+          </p>
           <?php
-          endif;
-          ?>
-        </span></p>
-        <?php
-        endforeach;
-        foreach ($f2b_data['perm_bans'] as $perm_bans):
-        ?>
-        <p>
-        <span class="label label-danger" style="padding:4px;font-size:85%;"><span class="glyphicon glyphicon-filter"></span> <?=$perm_bans?></span>
-        </p>
-        <?php
-        endforeach;
+          endforeach;
+        endif;
         ?>
       </div>
     </div>
@@ -645,9 +650,12 @@ $tfa_data = get_tfa();
           </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>
+              <legend data-target="#quarantine_template" style="cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+                <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['quarantine_notification_html'];?>
+              </legend>
+              <div id="quarantine_template" class="collapse" >
+                <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="html_tmpl"><?=$q_data['html_tmpl'];?></textarea>
+              </div>
             </div>
           </div>
           <div class="row">
@@ -680,13 +688,63 @@ $tfa_data = get_tfa();
       </div>
     </div>
 
+    <span class="anchor" id="quota"></span>
+    <div class="panel panel-default">
+      <div class="panel-heading">Quota notifications</div>
+      <div class="panel-body">
+      <p>Quota notications are sent to users once when crossing 80% and once when crossing 95% usage.</p>
+       <?php $q_data = quota_notification('get');?>
+      <form class="form" role="form" data-id="quota_notification" method="post">
+        <div class="row">
+          <div class="col-sm-6">
+            <div class="form-group">
+              <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label>
+              <input type="text" class="form-control" name="sender" value="<?=$q_data['sender'];?>" placeholder="quota-warning@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="Quota warning">
+            </div>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col-sm-12">
+            <legend data-target="#quota_template" style="cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+              <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['quarantine_notification_html'];?>
+            </legend>
+            <div id="quota_template" class="collapse" >
+              <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code collapse in" rows="20" name="html"><?=$q_data['html_tmpl'];?></textarea>
+            </div>
+          </div>
+        </div>
+        <div class="row">
+          <div class="col-sm-10">
+            <div class="form-group">
+              <br>
+              <a type="button" class="btn btn-sm btn-success" data-action="edit_selected"
+                data-item="quota_notification"
+                data-id="quota_notification"
+                data-api-url='edit/quota_notification'
+                data-api-attr='{}'><?=$lang['user']['save_changes'];?></a>
+            </div>
+          </div>
+        </div>
+      </form>
+      </div>
+    </div>
+
     <span class="anchor" id="rsettings"></span>
     <div class="panel panel-default">
       <div class="panel-heading">Rspamd settings map</div>
       <div class="panel-body">
-      <legend>Active settings map</legend>
-      <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>
+      <legend data-target="#active_settings_map" style="cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+        <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> Active settings map
+      </legend>
+      <div id="active_settings_map" class="collapse" >
+        <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>
+      </div>
       <?php $rsettings = rsettings('get'); ?>
         <form class="form" data-id="rsettings" role="form" method="post">
           <div class="row">
@@ -836,29 +894,33 @@ $tfa_data = get_tfa();
             <button class="btn btn-sm btn-default" type="button" id="add_app_link_row"><?=$lang['admin']['add_row'];?></button>
           </div></p>
         </form>
-        <legend><?=$lang['admin']['ui_texts'];?></legend>
+        <legend data-target="#ui_texts" style="cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['ui_texts'];?>
+        </legend>
+        <div id="ui_texts" class="collapse" >
         <?php
         $ui_texts = customize('get', 'ui_texts');
         ?>
-        <form class="form" data-id="uitexts" role="form" method="post">
-          <div class="form-group">
-            <label for="title_name"><?=$lang['admin']['title_name'];?>:</label>
-            <input type="text" class="form-control" name="title_name" placeholder="mailcow UI" value="<?=$ui_texts['title_name'];?>">
-          </div>
-          <div class="form-group">
-            <label for="main_name"><?=$lang['admin']['main_name'];?>:</label>
-            <input type="text" class="form-control" name="main_name" placeholder="mailcow UI" value="<?=$ui_texts['main_name'];?>">
-          </div>
-          <div class="form-group">
-            <label for="apps_name"><?=$lang['admin']['apps_name'];?>:</label>
-            <input type="text" class="form-control" name="apps_name" placeholder="mailcow Apps" value="<?=$ui_texts['apps_name'];?>">
-          </div>
-          <div class="form-group">
-            <label for="help_text"><?=$lang['admin']['help_text'];?>:</label>
-            <textarea class="form-control" id="help_text" name="help_text" rows="7"><?=$ui_texts['help_text'];?></textarea>
-          </div>
-          <button class="btn btn-default" data-action="edit_selected" data-item="ui" data-id="uitexts" data-api-url='edit/ui_texts' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
-        </form>
+          <form class="form" data-id="uitexts" role="form" method="post">
+            <div class="form-group">
+              <label for="title_name"><?=$lang['admin']['title_name'];?>:</label>
+              <input type="text" class="form-control" name="title_name" placeholder="mailcow UI" value="<?=$ui_texts['title_name'];?>">
+            </div>
+            <div class="form-group">
+              <label for="main_name"><?=$lang['admin']['main_name'];?>:</label>
+              <input type="text" class="form-control" name="main_name" placeholder="mailcow UI" value="<?=$ui_texts['main_name'];?>">
+            </div>
+            <div class="form-group">
+              <label for="apps_name"><?=$lang['admin']['apps_name'];?>:</label>
+              <input type="text" class="form-control" name="apps_name" placeholder="mailcow Apps" value="<?=$ui_texts['apps_name'];?>">
+            </div>
+            <div class="form-group">
+              <label for="help_text"><?=$lang['admin']['help_text'];?>:</label>
+              <textarea class="form-control" id="help_text" name="help_text" rows="7"><?=$ui_texts['help_text'];?></textarea>
+            </div>
+            <button class="btn btn-default" data-action="edit_selected" data-item="ui" data-id="uitexts" data-api-url='edit/ui_texts' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+          </form>
+        </div>
       </div>
     </div>
   </div>

BIN
data/web/fonts/PTS55F_W.woff


BIN
data/web/fonts/PTS56F_W.woff


BIN
data/web/fonts/PTS75F_W.woff


BIN
data/web/fonts/pt-sans-v9-cyrillic_latin-700.woff


BIN
data/web/fonts/pt-sans-v9-cyrillic_latin-700.woff2


BIN
data/web/fonts/pt-sans-v9-cyrillic_latin-italic.woff


BIN
data/web/fonts/pt-sans-v9-cyrillic_latin-italic.woff2


BIN
data/web/fonts/pt-sans-v9-cyrillic_latin-regular.woff


BIN
data/web/fonts/pt-sans-v9-cyrillic_latin-regular.woff2


+ 4 - 4
data/web/inc/functions.address_rewriting.inc.php

@@ -44,8 +44,8 @@ function bcc($_action, $_data = null, $attr = null) {
           );
           return false;
         }
-        $domain = idn_to_ascii($local_dest);
-        $local_dest_sane = '@' . idn_to_ascii($local_dest);
+        $domain = idn_to_ascii($local_dest, 0, INTL_IDNA_VARIANT_UTS46);
+        $local_dest_sane = '@' . idn_to_ascii($local_dest, 0, INTL_IDNA_VARIANT_UTS46);
       }
       elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) {
         if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) {
@@ -265,7 +265,7 @@ function recipient_map($_action, $_data = null, $attr = null) {
       $new_dest = strtolower(trim($_data['recipient_map_new']));
       $active = intval($_data['active']);
       if (is_valid_domain_name($old_dest)) {
-        $old_dest_sane = '@' . idn_to_ascii($old_dest);
+        $old_dest_sane = '@' . idn_to_ascii($old_dest, 0, INTL_IDNA_VARIANT_UTS46);
       }
       elseif (filter_var($old_dest, FILTER_VALIDATE_EMAIL)) {
         $old_dest_sane = $old_dest;
@@ -331,7 +331,7 @@ function recipient_map($_action, $_data = null, $attr = null) {
           continue;
         }
         if (is_valid_domain_name($old_dest)) {
-          $old_dest_sane = '@' . idn_to_ascii($old_dest);
+          $old_dest_sane = '@' . idn_to_ascii($old_dest, 0, INTL_IDNA_VARIANT_UTS46);
         }
         elseif (filter_var($old_dest, FILTER_VALIDATE_EMAIL)) {
           $old_dest_sane = $old_dest;

+ 6 - 3
data/web/inc/functions.docker.inc.php

@@ -32,7 +32,8 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
         $containers = json_decode($response, true);
         if (!empty($containers)) {
           foreach ($containers as $container) {
-            if ($container['Config']['Labels']['com.docker.compose.service'] == $service_name
+            if (isset($container['Config']['Labels']['com.docker.compose.service'])
+              && $container['Config']['Labels']['com.docker.compose.service'] == $service_name
               && $container['Config']['Labels']['com.docker.compose.project'] == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
               return trim($container['Id']);
             }
@@ -120,7 +121,8 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
         if (!empty($decoded_response)) {
           if (empty($service_name)) {
             foreach ($decoded_response as $container) {
-              if ($container['Config']['Labels']['com.docker.compose.project'] == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
+              if (isset($container['Config']['Labels']['com.docker.compose.project'])
+                && $container['Config']['Labels']['com.docker.compose.project'] == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
                 unset($container['Config']['Env']);
                 $out[$container['Config']['Labels']['com.docker.compose.service']]['State'] = $container['State'];
                 $out[$container['Config']['Labels']['com.docker.compose.service']]['Config'] = $container['Config'];
@@ -128,7 +130,8 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
             }
           }
           else {
-            if ($decoded_response['Config']['Labels']['com.docker.compose.project'] == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
+            if (isset($decoded_response['Config']['Labels']['com.docker.compose.project']) 
+              && $decoded_response['Config']['Labels']['com.docker.compose.project'] == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
               unset($container['Config']['Env']);
               $out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['State'] = $decoded_response['State'];
               $out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['Config'] = $decoded_response['Config'];

+ 1 - 0
data/web/inc/functions.fail2ban.inc.php

@@ -118,6 +118,7 @@ function fail2ban($_action, $_data = null) {
               if (valid_network($network)) {
                 $redis->hSet('F2B_BLACKLIST', $network, 1);
                 $redis->hDel('F2B_WHITELIST', $network, 1);
+                $response = docker('post', 'netfilter-mailcow', 'restart');
               }
             }
           }

+ 1 - 1
data/web/inc/functions.inc.php

@@ -680,7 +680,7 @@ function is_valid_domain_name($domain_name) {
 	if (empty($domain_name)) {
 		return false;
 	}
-	$domain_name = idn_to_ascii($domain_name);
+	$domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
 	return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
 		   && preg_match("/^.{1,253}$/", $domain_name)
 		   && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));

+ 13 - 13
data/web/inc/functions.mailbox.inc.php

@@ -322,7 +322,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          $domain				= idn_to_ascii(strtolower(trim($_data['domain'])));
+          $domain				= idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $description  = $_data['description'];
           $aliases			= $_data['aliases'];
           $mailboxes    = $_data['mailboxes'];
@@ -493,7 +493,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               if (empty($goto)) {
                 continue;
               }
-              $goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1));
+              $goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
               $goto_local_part = strstr($goto, '@', true);
               $goto = $goto_local_part.'@'.$goto_domain;
               $stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
@@ -531,7 +531,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             if (in_array($address, $gotos)) {
               continue;
             }
-            $domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
+            $domain       = idn_to_ascii(substr(strstr($address, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
             $local_part   = strstr($address, '@', true);
             $address      = $local_part.'@'.$domain;
             $domaindata = mailbox('get', 'domain_details', $domain);
@@ -637,7 +637,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $active = intval($_data['active']);
           $alias_domains  = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['alias_domain']));
           $alias_domains = array_filter($alias_domains);
-          $target_domain = idn_to_ascii(strtolower(trim($_data['target_domain'])));
+          $target_domain = idn_to_ascii(strtolower(trim($_data['target_domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           if (!isset($_SESSION['acl']['alias_domains']) || $_SESSION['acl']['alias_domains'] != "1" ) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
@@ -663,7 +663,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($alias_domains as $i => $alias_domain) {
-            $alias_domain = idn_to_ascii(strtolower(trim($alias_domain)));
+            $alias_domain = idn_to_ascii(strtolower(trim($alias_domain)), 0, INTL_IDNA_VARIANT_UTS46);
             if (!is_valid_domain_name($alias_domain)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
@@ -735,7 +735,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         break;
         case 'mailbox':
           $local_part   = strtolower(trim($_data['local_part']));
-          $domain       = idn_to_ascii(strtolower(trim($_data['domain'])));
+          $domain       = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $username     = $local_part . '@' . $domain;
           if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
             $_SESSION['return'][] = array(
@@ -938,7 +938,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           );
         break;
         case 'resource':
-          $domain             = idn_to_ascii(strtolower(trim($_data['domain'])));
+          $domain             = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $description        = $_data['description'];
           $local_part         = preg_replace('/[^\da-z]/i', '', preg_quote($description, '/'));
           $name               = $local_part . '@' . $domain;
@@ -1056,11 +1056,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         case 'alias_domain':
           $alias_domains = (array)$_data['alias_domain'];
           foreach ($alias_domains as $alias_domain) {
-            $alias_domain = idn_to_ascii(strtolower(trim($alias_domain)));
+            $alias_domain = idn_to_ascii(strtolower(trim($alias_domain)), 0, INTL_IDNA_VARIANT_UTS46);
             $is_now = mailbox('get', 'alias_domain_details', $alias_domain);
             if (!empty($is_now)) {
               $active         = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
-              $target_domain  = (!empty($_data['target_domain'])) ? idn_to_ascii(strtolower(trim($_data['target_domain']))) : $is_now['target_domain'];
+              $target_domain  = (!empty($_data['target_domain'])) ? idn_to_ascii(strtolower(trim($_data['target_domain'])), 0, INTL_IDNA_VARIANT_UTS46) : $is_now['target_domain'];
             }
             else {
               $_SESSION['return'][] = array(
@@ -1676,7 +1676,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               continue;
             }
             if ($is_now['address'] != $address) {
-              $domain = idn_to_ascii(substr(strstr($address, '@'), 1));
+              $domain = idn_to_ascii(substr(strstr($address, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
               $local_part = strstr($address, '@', true);
               $address      = $local_part.'@'.$domain;
               if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
@@ -1810,7 +1810,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $domains = $_data['domain'];
           }
           foreach ($domains as $domain) {
-            $domain = idn_to_ascii($domain);
+            $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
             if (!is_valid_domain_name($domain)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
@@ -2887,7 +2887,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         break;
         case 'domain_details':
           $domaindata = array();
-          $_data = idn_to_ascii(strtolower(trim($_data)));
+          $_data = idn_to_ascii(strtolower(trim($_data)), 0, INTL_IDNA_VARIANT_UTS46);
           if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
             return false;
           }
@@ -3308,7 +3308,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $domain	= idn_to_ascii(strtolower(trim($domain)));
+            $domain	= idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
             $stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
               WHERE `domain` = :domain");
             $stmt->execute(array(':domain' => $domain));

+ 3 - 3
data/web/inc/functions.policy.inc.php

@@ -26,7 +26,7 @@ function policy($_action, $_scope, $_data = null) {
               );
               return false;
             }
-            $object = idn_to_ascii(strtolower(trim($object)));
+            $object = idn_to_ascii(strtolower(trim($object)), 0, INTL_IDNA_VARIANT_UTS46);
           }
           else {
             $_SESSION['return'][] = array(
@@ -183,7 +183,7 @@ function policy($_action, $_scope, $_data = null) {
                 );
                 continue;
               }
-              $object = idn_to_ascii(strtolower(trim($object)));
+              $object = idn_to_ascii(strtolower(trim($object)), 0, INTL_IDNA_VARIANT_UTS46);
             }
             else {
               $_SESSION['return'][] = array(
@@ -277,7 +277,7 @@ function policy($_action, $_scope, $_data = null) {
             if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
               return false;
             }
-            $_data = idn_to_ascii(strtolower(trim($_data)));
+            $_data = idn_to_ascii(strtolower(trim($_data)), 0, INTL_IDNA_VARIANT_UTS46);
           }
 
           // WHITELIST

+ 4 - 4
data/web/inc/functions.quarantine.inc.php

@@ -83,7 +83,7 @@ function quarantine($_action, $_data = null) {
         $max_size = $_data['max_size'];
         $subject = $_data['subject'];
         $sender = $_data['sender'];
-        $html = $_data['html'];
+        $html = $_data['html_tmpl'];
         $exclude_domains = (array)$_data['exclude_domains'];
         try {
           $redis->Set('Q_RETENTION_SIZE', intval($retention_size));
@@ -436,9 +436,9 @@ function quarantine($_action, $_data = null) {
         $settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT');
         $settings['subject'] = $redis->Get('Q_SUBJ');
         $settings['sender'] = $redis->Get('Q_SENDER');
-        $settings['html'] = htmlspecialchars($redis->Get('Q_HTML'));
-        if (empty($settings['html'])) {
-          $settings['html'] = htmlspecialchars(file_get_contents("/tpls/quarantine.tpl"));
+        $settings['html_tmpl'] = htmlspecialchars($redis->Get('Q_HTML'));
+        if (empty($settings['html_tmpl'])) {
+          $settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quarantine.tpl"));
         }
       }
       catch (RedisException $e) {

+ 65 - 0
data/web/inc/functions.quota_notification.inc.php

@@ -0,0 +1,65 @@
+<?php
+function quota_notification($_action, $_data = null) {
+	global $redis;
+	global $lang;
+	$_data_log = $_data;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'][] = array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_action, $_data_log),
+      'msg' => 'access_denied'
+    );
+    return false;
+  }
+  switch ($_action) {
+    case 'edit':
+      $retention_size = $_data['retention_size'];
+      if ($_data['release_format'] == 'attachment' || $_data['release_format'] == 'raw') {
+        $release_format = $_data['release_format'];
+      }
+      else {
+        $release_format = 'raw';
+      }
+      $subject = $_data['subject'];
+      $sender = $_data['sender'];
+      $html = $_data['html_tmpl'];
+      try {
+        $redis->Set('QW_SENDER', $sender);
+        $redis->Set('QW_SUBJ', $subject);
+        $redis->Set('QW_HTML', $html);
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => array('redis_error', $e)
+        );
+        return false;
+      }
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data_log),
+        'msg' => 'saved_settings'
+      );
+    break;
+    case 'get':
+      try {
+        $settings['subject'] = $redis->Get('QW_SUBJ');
+        $settings['sender'] = $redis->Get('QW_SENDER');
+        $settings['html_tmpl'] = htmlspecialchars($redis->Get('QW_HTML'));
+        if (empty($settings['html_tmpl'])) {
+          $settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quota.tpl"));
+        }
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => array('redis_error', $e)
+        );
+        return false;
+      }
+      return $settings;
+    break;
+  }
+}

+ 1 - 1
data/web/inc/functions.tls_policy_maps.inc.php

@@ -7,7 +7,7 @@ function tls_policy_maps($_action, $_data = null, $attr = null) {
   }
   switch ($_action) {
     case 'add':
-      $dest = idn_to_ascii(trim($_data['dest']));
+      $dest = idn_to_ascii(trim($_data['dest']), 0, INTL_IDNA_VARIANT_UTS46);
       $policy = strtolower(trim($_data['policy']));
       $parameters = (isset($_data['parameters']) && !empty($_data['parameters'])) ? $_data['parameters'] : '';
       if (!empty($parameters)) {

+ 7 - 9
data/web/inc/header.inc.php

@@ -7,10 +7,6 @@
   <meta name="theme-color" content="#F5D76E"/>
   <meta http-equiv="Referrer-Policy" content="same-origin">
   <title><?=$UI_TEXTS['title_name'];?></title>
-  <!--[if lt IE 9]>
-    <script src="/js/html5shiv.min.js"></script>
-    <script src="/js/respond.min.js"></script>
-  <![endif]-->
   <?php if (strtolower(trim($DEFAULT_THEME)) != "lumen"): ?>
   <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/<?= strtolower(trim($DEFAULT_THEME)); ?>/bootstrap.min.css">
   <?php endif;
@@ -117,12 +113,14 @@
             <?php
             endforeach;
             $app_links = customize('get', 'app_links');
-            foreach ($app_links as $row) {
-              foreach ($row as $key => $val):
-            ?>
+            if ($app_links) {
+              foreach ($app_links as $row) {
+                foreach ($row as $key => $val):
+              ?>
               <li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></a></li>
-            <?php
-              endforeach;
+              <?php
+                endforeach;
+              }
             }
             ?>
             </ul>

+ 1 - 0
data/web/inc/prerequisites.inc.php

@@ -157,6 +157,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.address_rewriting.inc.p
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.admin.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantine.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quota_notification.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';

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

@@ -44,18 +44,18 @@ $autodiscover_config = array(
   // The autoconfig service will additionally announce the STARTTLS-enabled ports, specified in the "tlsport" variable.
   'imap' => array(
     'server' => $mailcow_hostname,
-    'port' => array_pop(explode(':', getenv('IMAPS_PORT'))),
-    'tlsport' => array_pop(explode(':', getenv('IMAP_PORT'))),
+    'port' => array_key_last(explode(':', getenv('IMAPS_PORT'))),
+    'tlsport' => array_key_last(explode(':', getenv('IMAP_PORT'))),
   ),
   'pop3' => array(
     'server' => $mailcow_hostname,
-    'port' => array_pop(explode(':', getenv('POPS_PORT'))),
-    'tlsport' => array_pop(explode(':', getenv('POP_PORT'))),
+    'port' => array_key_last(explode(':', getenv('POPS_PORT'))),
+    'tlsport' => array_key_last(explode(':', getenv('POP_PORT'))),
   ),
   'smtp' => array(
     'server' => $mailcow_hostname,
-    'port' => array_pop(explode(':', getenv('SMTPS_PORT'))),
-    'tlsport' => array_pop(explode(':', getenv('SUBMISSION_PORT'))),
+    'port' => array_key_last(explode(':', getenv('SMTPS_PORT'))),
+    'tlsport' => array_key_last(explode(':', getenv('SUBMISSION_PORT'))),
   ),
   'activesync' => array(
     'url' => 'https://'.$mailcow_hostname.($https_port == 443 ? '' : ':'.$https_port).'/Microsoft-Server-ActiveSync',

+ 3 - 0
data/web/json_api.php

@@ -1161,6 +1161,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "quarantine":
             process_edit_return(quarantine('edit', $attr));
           break;
+          case "quota_notification":
+            process_edit_return(quota_notification('edit', $attr));
+          break;
           case "mailq":
             process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
           break;

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

@@ -642,8 +642,11 @@ $lang['admin']['quarantine_retention_size'] = "Rückhaltungen pro Mailbox:<br><s
 $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']['quota_notification_sender'] = "Benachrichtigungs-E-Mail Absender";
 $lang['admin']['quarantine_notification_subject'] = "Benachrichtigungs-E-Mail Betreff";
+$lang['admin']['quota_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']['quota_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";

+ 3 - 0
data/web/lang/lang.en.php

@@ -682,6 +682,9 @@ $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']['quota_notification_sender'] = "Notification email sender";
+$lang['admin']['quota_notification_subject'] = "Notification email subject";
+$lang['admin']['quota_notification_html'] = "Notification email template:<br><small>Leave empty to restore default template.</small>";
 $lang['admin']['ui_texts'] = "UI labels and texts";
 $lang['admin']['help_text'] = "Override help text below login mask (HTML allowed)";
 $lang['admin']['title_name'] = '"mailcow UI" website title';