瀏覽代碼

Merge branch 'feature/bootstrap5' into nightly

DerLinkman 2 年之前
父節點
當前提交
a09661fc83
共有 43 個文件被更改,包括 2761 次插入199 次删除
  1. 1 0
      data/Dockerfiles/dockerapi/dockerapi.py
  2. 64 39
      data/web/edit.php
  3. 582 4
      data/web/inc/functions.mailbox.inc.php
  4. 106 1
      data/web/inc/init_db.inc.php
  5. 33 90
      data/web/js/build/014-mailcow.js
  6. 68 7
      data/web/js/site/debug.js
  7. 11 0
      data/web/js/site/edit.js
  8. 700 0
      data/web/js/site/mailbox.js
  9. 58 6
      data/web/json_api.php
  10. 6 0
      data/web/lang/lang.ca-es.json
  11. 13 0
      data/web/lang/lang.cs-cz.json
  12. 9 1
      data/web/lang/lang.da-dk.json
  13. 13 0
      data/web/lang/lang.de-de.json
  14. 24 0
      data/web/lang/lang.en-gb.json
  15. 11 0
      data/web/lang/lang.es-es.json
  16. 12 1
      data/web/lang/lang.fi-fi.json
  17. 12 1
      data/web/lang/lang.fr-fr.json
  18. 1 0
      data/web/lang/lang.hu-hu.json
  19. 11 0
      data/web/lang/lang.it-it.json
  20. 11 0
      data/web/lang/lang.ko-kr.json
  21. 7 0
      data/web/lang/lang.lv-lv.json
  22. 11 0
      data/web/lang/lang.nl-nl.json
  23. 6 0
      data/web/lang/lang.pl-pl.json
  24. 7 1
      data/web/lang/lang.pt-pt.json
  25. 13 0
      data/web/lang/lang.ro-ro.json
  26. 13 0
      data/web/lang/lang.ru-ru.json
  27. 13 0
      data/web/lang/lang.sk-sk.json
  28. 13 0
      data/web/lang/lang.sv-se.json
  29. 13 0
      data/web/lang/lang.uk-ua.json
  30. 11 0
      data/web/lang/lang.zh-cn.json
  31. 13 0
      data/web/lang/lang.zh-tw.json
  32. 1 0
      data/web/mailbox.php
  33. 4 4
      data/web/templates/base.twig
  34. 6 12
      data/web/templates/debug.twig
  35. 136 0
      data/web/templates/edit/domain-templates.twig
  36. 7 1
      data/web/templates/edit/domain.twig
  37. 169 0
      data/web/templates/edit/mailbox-templates.twig
  38. 7 1
      data/web/templates/edit/mailbox.twig
  39. 6 3
      data/web/templates/mailbox.twig
  40. 51 0
      data/web/templates/mailbox/tab-templates-domains.twig
  41. 51 0
      data/web/templates/mailbox/tab-templates-mbox.twig
  42. 4 4
      data/web/templates/modals/footer.twig
  43. 453 23
      data/web/templates/modals/mailbox.twig

+ 1 - 0
data/Dockerfiles/dockerapi/dockerapi.py

@@ -499,6 +499,7 @@ class DockerUtils:
           async with rspamd_password_exec.start(detach=False) as stream:
             rspamd_password_return = await stream.read_out()
 
+          matched = False
           if "OK" in rspamd_password_return.data.decode('utf-8'):
             matched = True
             await container.restart()

+ 64 - 39
data/web/edit.php

@@ -38,24 +38,46 @@ if (isset($_SESSION['mailcow_cc_role'])) {
       $template = 'edit/admin.twig';
       $template_data = ['admin' => $admin];
     }
-    elseif (isset($_GET['domain']) &&
-      is_valid_domain_name($_GET["domain"]) &&
-      !empty($_GET["domain"])) {
-        $domain = $_GET["domain"];
-        $result = mailbox('get', 'domain_details', $domain);
-        $quota_notification_bcc = quota_notification_bcc('get', $domain);
-        $rl = ratelimit('get', 'domain', $domain);
-        $rlyhosts = relayhost('get');
-        $template = 'edit/domain.twig';
+    elseif (isset($_GET['domain'])) {
+      if (is_valid_domain_name($_GET["domain"]) &&
+        !empty($_GET["domain"])) {
+          // edit domain
+          $domain = $_GET["domain"];
+          $result = mailbox('get', 'domain_details', $domain);
+          $quota_notification_bcc = quota_notification_bcc('get', $domain);
+          $rl = ratelimit('get', 'domain', $domain);
+          $rlyhosts = relayhost('get');
+          $template = 'edit/domain.twig';
+          $template_data = [
+            'acl' => $_SESSION['acl'],
+            'domain' => $domain,
+            'quota_notification_bcc' => $quota_notification_bcc,
+            'rl' => $rl,
+            'rlyhosts' => $rlyhosts,
+            'dkim' => dkim('details', $domain),
+            'domain_details' => $result,
+          ];
+      }
+    }
+    elseif (isset($_GET["template"])){
+      $domain_template = mailbox('get', 'domain_templates', $_GET["template"]);
+      if ($domain_template){
         $template_data = [
-          'acl' => $_SESSION['acl'],
-          'domain' => $domain,
-          'quota_notification_bcc' => $quota_notification_bcc,
-          'rl' => $rl,
-          'rlyhosts' => $rlyhosts,
-          'dkim' => dkim('details', $domain),
-          'domain_details' => $result,
+          'template' => $domain_template
         ];
+        $template = 'edit/domain-templates.twig';
+        $result = true;
+      }
+      else {
+        $mailbox_template = mailbox('get', 'mailbox_templates', $_GET["template"]);
+        if ($mailbox_template){
+          $template_data = [
+            'template' => $mailbox_template
+          ];
+          $template = 'edit/mailbox-templates.twig';
+          $result = true;
+        }
+      }
     }
     elseif (isset($_GET['oauth2client']) &&
       is_numeric($_GET["oauth2client"]) &&
@@ -79,29 +101,32 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           'dkim' => dkim('details', $alias_domain),
         ];
     }
-    elseif (isset($_GET['mailbox']) && filter_var(html_entity_decode(rawurldecode($_GET["mailbox"])), FILTER_VALIDATE_EMAIL) && !empty($_GET["mailbox"])) {
-      $mailbox = html_entity_decode(rawurldecode($_GET["mailbox"]));
-      $result = mailbox('get', 'mailbox_details', $mailbox);
-      $rl = ratelimit('get', 'mailbox', $mailbox);
-      $pushover_data = pushover('get', $mailbox);
-      $quarantine_notification = mailbox('get', 'quarantine_notification', $mailbox);
-      $quarantine_category = mailbox('get', 'quarantine_category', $mailbox);
-      $get_tls_policy = mailbox('get', 'tls_policy', $mailbox);
-      $rlyhosts = relayhost('get');
-      $template = 'edit/mailbox.twig';
-      $template_data = [
-        'acl' => $_SESSION['acl'],
-        'mailbox' => $mailbox,
-        'rl' => $rl,
-        'pushover_data' => $pushover_data,
-        'quarantine_notification' => $quarantine_notification,
-        'quarantine_category' => $quarantine_category,
-        'get_tls_policy' => $get_tls_policy,
-        'rlyhosts' => $rlyhosts,
-        'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
-        'user_acls' => acl('get', 'user', $mailbox),
-        'mailbox_details' => $result
-      ];
+    elseif (isset($_GET['mailbox'])){
+      if(filter_var(html_entity_decode(rawurldecode($_GET["mailbox"])), FILTER_VALIDATE_EMAIL) && !empty($_GET["mailbox"])) {
+        // edit mailbox
+        $mailbox = html_entity_decode(rawurldecode($_GET["mailbox"]));
+        $result = mailbox('get', 'mailbox_details', $mailbox);
+        $rl = ratelimit('get', 'mailbox', $mailbox);
+        $pushover_data = pushover('get', $mailbox);
+        $quarantine_notification = mailbox('get', 'quarantine_notification', $mailbox);
+        $quarantine_category = mailbox('get', 'quarantine_category', $mailbox);
+        $get_tls_policy = mailbox('get', 'tls_policy', $mailbox);
+        $rlyhosts = relayhost('get');
+        $template = 'edit/mailbox.twig';
+        $template_data = [
+          'acl' => $_SESSION['acl'],
+          'mailbox' => $mailbox,
+          'rl' => $rl,
+          'pushover_data' => $pushover_data,
+          'quarantine_notification' => $quarantine_notification,
+          'quarantine_category' => $quarantine_category,
+          'get_tls_policy' => $get_tls_policy,
+          'rlyhosts' => $rlyhosts,
+          'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
+          'user_acls' => acl('get', 'user', $mailbox),
+          'mailbox_details' => $result
+        ];
+      }
     }
     elseif (isset($_GET['relayhost']) && is_numeric($_GET["relayhost"]) && !empty($_GET["relayhost"])) {
         $relayhost = intval($_GET["relayhost"]);

+ 582 - 4
data/web/inc/functions.mailbox.inc.php

@@ -1020,6 +1020,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           if (empty($name)) {
             $name = $local_part;
           }
+          if (isset($_data['protocol_access'])) {
+            $_data['protocol_access'] = (array)$_data['protocol_access'];
+            $_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
+            $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
+            $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
+            $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
+          }
           $active = intval($_data['active']);
           $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
           $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
@@ -1200,10 +1207,63 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':domain' => $domain,
             ':active' => $active
           ));
-          $stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`) VALUES (:username)");
-          $stmt->execute(array(
-            ':username' => $username
-          ));
+
+          
+          if (isset($_data['acl'])) {
+            $_data['acl'] = (array)$_data['acl'];
+            $_data['spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
+            $_data['tls_policy'] = (in_array('tls_policy', $_data['acl'])) ? 1 : 0;
+            $_data['spam_score'] = (in_array('spam_score', $_data['acl'])) ? 1 : 0;
+            $_data['spam_policy'] = (in_array('spam_policy', $_data['acl'])) ? 1 : 0;
+            $_data['delimiter_action'] = (in_array('delimiter_action', $_data['acl'])) ? 1 : 0;
+            $_data['syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
+            $_data['eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
+            $_data['sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
+            $_data['pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
+            $_data['quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
+            $_data['quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
+            $_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
+            $_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
+            $_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
+
+            $stmt = $pdo->prepare("INSERT INTO `user_acl` 
+              (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
+               `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) 
+              VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
+               :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
+            $stmt->execute(array(
+              ':username' => $username,
+              ':spam_alias' => $_data['spam_alias'],
+              ':tls_policy' => $_data['tls_policy'],
+              ':spam_score' => $_data['spam_score'],
+              ':spam_policy' => $_data['spam_policy'],
+              ':delimiter_action' => $_data['delimiter_action'],
+              ':syncjobs' => $_data['syncjobs'],
+              ':eas_reset' => $_data['eas_reset'],
+              ':sogo_profile_reset' => $_data['sogo_profile_reset'],
+              ':pushover' => $_data['pushover'],
+              ':quarantine' => $_data['quarantine'],
+              ':quarantine_attachments' => $_data['quarantine_attachments'],
+              ':quarantine_notification' => $_data['quarantine_notification'],
+              ':quarantine_category' => $_data['quarantine_category'],
+              ':app_passwds' => $_data['app_passwds']
+            ));
+          }
+          else {
+            $stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`) VALUES (:username)");
+            $stmt->execute(array(
+              ':username' => $username
+            ));
+          }
+
+          if (isset($_data['rl_frame']) && isset($_data['rl_value'])){
+            ratelimit('edit', 'mailbox', array(
+              'object' => $username,
+              'rl_frame' => $_data['rl_frame'],
+              'rl_value' => $_data['rl_value']
+            ));
+          }
+
           $_SESSION['return'][] = array(
             'type' => 'success',
             'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1322,6 +1382,191 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             'msg' => array('resource_added', htmlspecialchars($name))
           );
         break;
+        case 'domain_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (empty($_data["template"])){
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => 'template_name_invalid'
+            );
+            return false;
+          }
+
+          // check if template name exists, return false
+          $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template");
+          $stmt->execute(array(
+            ":type" => "domain",
+            ":template" => $_data["template"]
+          ));
+          $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+          if (!empty($row)){
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => array('template_exists', $_data["template"])
+            );
+            return false;
+          }
+          
+          // check attributes
+          $attr = array();
+          $attr['tags']                       = (isset($_data['tags'])) ? $_data['tags'] : array();
+          $attr['max_num_aliases_for_domain'] = (isset($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 0;
+          $attr['max_num_mboxes_for_domain']  = (isset($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 0;
+          $attr['def_quota_for_mbox']         = (isset($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 0;
+          $attr['max_quota_for_mbox']         = (isset($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 0;
+          $attr['max_quota_for_domain']       = (isset($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 0;
+          $attr['rl_frame']                   = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
+          $attr['rl_value']                   = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
+          $attr['active']                     = isset($_data['active']) ? intval($_data['active']) : 1;
+          $attr['gal']                        = (isset($_data['gal'])) ? intval($_data['gal']) : 1;
+          $attr['backupmx']                   = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : 0;
+          $attr['relay_all_recipients']       = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : 0;
+          $attr['relay_unknown_only']          = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : 0;
+          $attr['dkim_selector']              = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim";
+          $attr['key_size']                   = isset($_data['key_size']) ? intval($_data['key_size']) : 2048;
+
+
+          // save template
+          $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`)
+            VALUES (:type, :template, :attributes)");
+          $stmt->execute(array(
+            ":type" => "domain",
+            ":template" => $_data["template"],
+            ":attributes" => json_encode($attr)
+          ));
+
+          // success
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => array('template_added', $_data["template"])
+          );
+          return true;
+        break;
+        case 'mailbox_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (empty($_data["template"])){
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => 'template_name_invalid'
+            );
+            return false;
+          }
+
+          // check if template name exists, return false
+          $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template");
+          $stmt->execute(array(
+            ":type" => "mailbox",
+            ":template" => $_data["template"]
+          ));
+          $row = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (!empty($row)){
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => array('template_exists', $_data["template"])
+            );
+            return false;
+          }
+
+
+          // check attributes
+          $attr = array();
+          $attr["quota"]                       = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
+          $attr['tags']                        = (isset($_data['tags'])) ? $_data['tags'] : array();
+          $attr["quarantine_notification"]     = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
+          $attr["quarantine_category"]         = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
+          $attr["rl_frame"]                    = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
+          $attr["rl_value"]                    = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
+          $attr["force_pw_update"]             = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
+          $attr["sogo_access"]                 = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
+          $attr["active"]                      = isset($_data['active']) ? intval($_data['active']) : 1;
+          $attr["tls_enforce_in"]              = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
+          $attr["tls_enforce_out"]             = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
+          if (isset($_data['protocol_access'])) {
+            $_data['protocol_access'] = (array)$_data['protocol_access'];
+            $attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
+            $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
+            $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
+            $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
+          }   
+          else {
+            $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
+            $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
+            $attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
+            $attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
+          }       
+          if (isset($_data['acl'])) {
+            $_data['acl'] = (array)$_data['acl'];
+            $attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
+            $attr['acl_tls_policy'] = (in_array('tls_policy', $_data['acl'])) ? 1 : 0;
+            $attr['acl_spam_score'] = (in_array('spam_score', $_data['acl'])) ? 1 : 0;
+            $attr['acl_spam_policy'] = (in_array('spam_policy', $_data['acl'])) ? 1 : 0;
+            $attr['acl_delimiter_action'] = (in_array('delimiter_action', $_data['acl'])) ? 1 : 0;
+            $attr['acl_syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
+            $attr['acl_eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
+            $attr['acl_sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
+            $attr['acl_pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
+            $attr['acl_quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
+            $attr['acl_quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
+            $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
+            $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
+            $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
+          } else {
+            $_data['acl'] = (array)$_data['acl'];
+            $attr['acl_spam_alias'] = 1;
+            $attr['acl_tls_policy'] = 1;
+            $attr['acl_spam_score'] = 1;
+            $attr['acl_spam_policy'] = 1;
+            $attr['acl_delimiter_action'] = 1;
+            $attr['acl_syncjobs'] = 0;
+            $attr['acl_eas_reset'] = 1;
+            $attr['acl_sogo_profile_reset'] = 0;
+            $attr['acl_pushover'] = 1;
+            $attr['acl_quarantine'] = 1;
+            $attr['acl_quarantine_attachments'] = 1;
+            $attr['acl_quarantine_notification'] = 1;
+            $attr['acl_quarantine_category'] = 1;
+            $attr['acl_app_passwds'] = 1;
+          }
+
+
+
+          // save template
+          $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`)
+          VALUES (:type, :template, :attributes)");
+          $stmt->execute(array(
+            ":type" => "mailbox",
+            ":template" => $_data["template"],
+            ":attributes" => json_encode($attr)
+          ));
+
+          // success
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => array('template_added', $_data["template"])
+          );
+          return true;
+        break;
       }
     break;
     case 'edit':
@@ -2472,6 +2717,79 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
           }
         break;
+        case 'domain_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (!is_array($_data['ids'])) {
+            $ids = array();
+            $ids[] = $_data['ids'];
+          }
+          else {
+            $ids = $_data['ids'];
+          }
+          foreach ($ids as $id) {
+            $is_now = mailbox("get", "domain_templates", $id);
+            if (empty($is_now) ||
+                $is_now["type"] != "domain"){
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+                'msg' => 'template_id_invalid'
+              );
+              continue;
+            }
+
+            // check name
+            if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){
+              // keep template name of Default template
+              $_data["template"]                   = $is_now["template"]; 
+            }
+            else {
+              $_data["template"]                   = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; 
+            }   
+            // check attributes
+            $attr = array();
+            $attr['tags']                       = (isset($_data['tags'])) ? $_data['tags'] : array();
+            $attr['max_num_aliases_for_domain'] = (isset($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 0;
+            $attr['max_num_mboxes_for_domain']  = (isset($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 0;
+            $attr['def_quota_for_mbox']         = (isset($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 0;
+            $attr['max_quota_for_mbox']         = (isset($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 0;
+            $attr['max_quota_for_domain']       = (isset($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 0;
+            $attr['rl_frame']                   = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
+            $attr['rl_value']                   = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
+            $attr['active']                     = isset($_data['active']) ? intval($_data['active']) : 1;
+            $attr['gal']                        = (isset($_data['gal'])) ? intval($_data['gal']) : 1;
+            $attr['backupmx']                   = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : 0;
+            $attr['relay_all_recipients']       = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : 0;
+            $attr['relay_unknown_only']          = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : 0;
+            $attr['dkim_selector']              = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim";
+            $attr['key_size']                   = isset($_data['key_size']) ? intval($_data['key_size']) : 2048;
+
+            // update template
+            $stmt = $pdo->prepare("UPDATE `templates`
+              SET `template` = :template, `attributes` = :attributes
+              WHERE id = :id");
+            $stmt->execute(array(
+              ":id" => $id ,
+              ":template" => $_data["template"] ,
+              ":attributes" => json_encode($attr)
+            )); 
+          }
+
+  
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => array('template_modified', $_data["template"])
+          );
+          return true;
+        break;
         case 'mailbox':
           if (!is_array($_data['username'])) {
             $usernames = array();
@@ -2814,6 +3132,110 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
           }
         break;
+        case 'mailbox_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (!is_array($_data['ids'])) {
+            $ids = array();
+            $ids[] = $_data['ids'];
+          }
+          else {
+            $ids = $_data['ids'];
+          }
+          foreach ($ids as $id) {
+            $is_now = mailbox("get", "mailbox_templates", $id);
+            if (empty($is_now) ||
+                $is_now["type"] != "mailbox"){
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
+                'msg' => 'template_id_invalid'
+              );
+              continue;
+            }
+
+
+            // check name
+            if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){
+              // keep template name of Default template
+              $_data["template"]                   = $is_now["template"]; 
+            }
+            else {
+              $_data["template"]                   = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; 
+            }   
+            // check attributes
+            $attr = array();
+            $attr["quota"]                       = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
+            $attr['tags']                        = (isset($_data['tags'])) ? $_data['tags'] : $is_now['tags'];
+            $attr["quarantine_notification"]     = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
+            $attr["quarantine_category"]         = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
+            $attr["rl_frame"]                    = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
+            $attr["rl_value"]                    = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
+            $attr["force_pw_update"]             = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
+            $attr["sogo_access"]                 = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
+            $attr["active"]                      = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
+            $attr["tls_enforce_in"]              = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
+            $attr["tls_enforce_out"]             = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out'];
+            if (isset($_data['protocol_access'])) {
+              $_data['protocol_access'] = (array)$_data['protocol_access'];
+              $attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
+              $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
+              $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
+              $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
+            }          
+            else { 
+              foreach ($is_now as $key => $value){
+                $attr[$key] = $is_now[$key];
+              }    
+            }
+            if (isset($_data['acl'])) {
+              $_data['acl'] = (array)$_data['acl'];
+              $attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
+              $attr['acl_tls_policy'] = (in_array('tls_policy', $_data['acl'])) ? 1 : 0;
+              $attr['acl_spam_score'] = (in_array('spam_score', $_data['acl'])) ? 1 : 0;
+              $attr['acl_spam_policy'] = (in_array('spam_policy', $_data['acl'])) ? 1 : 0;
+              $attr['acl_delimiter_action'] = (in_array('delimiter_action', $_data['acl'])) ? 1 : 0;
+              $attr['acl_syncjobs'] = (in_array('syncjobs', $_data['acl'])) ? 1 : 0;
+              $attr['acl_eas_reset'] = (in_array('eas_reset', $_data['acl'])) ? 1 : 0;
+              $attr['acl_sogo_profile_reset'] = (in_array('sogo_profile_reset', $_data['acl'])) ? 1 : 0;
+              $attr['acl_pushover'] = (in_array('pushover', $_data['acl'])) ? 1 : 0;
+              $attr['acl_quarantine'] = (in_array('quarantine', $_data['acl'])) ? 1 : 0;
+              $attr['acl_quarantine_attachments'] = (in_array('quarantine_attachments', $_data['acl'])) ? 1 : 0;
+              $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
+              $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
+              $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
+            } else {    
+              foreach ($is_now as $key => $value){
+                $attr[$key] = $is_now[$key];
+              }        
+            }
+
+
+            // update template
+            $stmt = $pdo->prepare("UPDATE `templates`
+              SET `template` = :template, `attributes` = :attributes
+              WHERE id = :id");
+            $stmt->execute(array(
+              ":id" => $id ,
+              ":template" => $_data["template"] ,
+              ":attributes" => json_encode($attr)
+            )); 
+          }
+
+
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => array('template_modified', $_data["template"])
+          );
+          return true;
+        break;
         case 'resource':
           if (!is_array($_data['name'])) {
             $names = array();
@@ -3606,6 +4028,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailboxes`,
               `defquota`,
               `maxquota`,
+              `created`,
+              `modified`,
               `quota`,
               `relayhost`,
               `relay_all_recipients`,
@@ -3678,6 +4102,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients'];
           $domaindata['relay_unknown_only'] = $row['relay_unknown_only'];
           $domaindata['relay_unknown_only_int'] = $row['relay_unknown_only'];
+          $domaindata['created'] = $row['created'];
+          $domaindata['modified'] = $row['modified'];
           $stmt = $pdo->prepare("SELECT COUNT(`address`) AS `alias_count` FROM `alias`
             WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2))
               AND `address` NOT IN (
@@ -3711,6 +4137,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 
           return $domaindata;
         break;
+        case 'domain_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+            return false;
+          }
+          $_data = (isset($_data)) ? intval($_data) : null;
+
+          if (isset($_data)){          
+            $stmt = $pdo->prepare("SELECT * FROM `templates` 
+              WHERE `id` = :id AND type = :type");
+            $stmt->execute(array(
+              ":id" => $_data,
+              ":type" => "domain"
+            ));
+            $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  
+            if (empty($row)){
+              return false;
+            }
+  
+            $row["attributes"] = json_decode($row["attributes"], true);
+            return $row;
+          }
+          else {
+            $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` =  'domain'");
+            $stmt->execute();
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  
+            if (empty($rows)){
+              return false;
+            }
+  
+            foreach($rows as $key => $row){
+              $rows[$key]["attributes"] = json_decode($row["attributes"], true);
+            }
+            return $rows;
+          }
+        break;
         case 'mailbox_details':
           if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
             return false;
@@ -3725,6 +4188,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`domain`,
               `mailbox`.`local_part`,
               `mailbox`.`quota`,
+              `mailbox`.`created`,
+              `mailbox`.`modified`,
               `quota2`.`bytes`,
               `attributes`,
               `quota2`.`messages`
@@ -3743,6 +4208,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`domain`,
               `mailbox`.`local_part`,
               `mailbox`.`quota`,
+              `mailbox`.`created`,
+              `mailbox`.`modified`,
               `quota2replica`.`bytes`,
               `attributes`,
               `quota2replica`.`messages`
@@ -3769,6 +4236,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['attributes'] = json_decode($row['attributes'], true);
           $mailboxdata['quota_used'] = intval($row['bytes']);
           $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
+          $mailboxdata['created'] = $row['created'];
+          $mailboxdata['modified'] = $row['modified'];
 
           if ($mailboxdata['percent_in_use'] === '- ') {
             $mailboxdata['percent_class'] = "info";
@@ -3856,6 +4325,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 
           return $mailboxdata;
         break;
+        case 'mailbox_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+            return false;
+          }
+          $_data = (isset($_data)) ? intval($_data) : null;
+
+          if (isset($_data)){          
+            $stmt = $pdo->prepare("SELECT * FROM `templates` 
+              WHERE `id` = :id AND type = :type");
+            $stmt->execute(array(
+              ":id" => $_data,
+              ":type" => "mailbox"
+            ));
+            $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  
+            if (empty($row)){
+              return false;
+            }
+  
+            $row["attributes"] = json_decode($row["attributes"], true);
+            return $row;
+          }
+          else {
+            $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` =  'mailbox'");
+            $stmt->execute();
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+            if (empty($rows)){
+              return false;
+            }
+
+            foreach($rows as $key => $row){
+              $rows[$key]["attributes"] = json_decode($row["attributes"], true);
+            }
+            return $rows;
+          }
+        break;
         case 'resource_details':
           $resourcedata = array();
           if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
@@ -4224,6 +4730,42 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
           }
         break;
+        case 'domain_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (!is_array($_data['ids'])) {
+            $ids = array();
+            $ids[] = $_data['ids'];
+          }
+          else {
+            $ids = $_data['ids'];
+          }
+
+          
+          foreach ($ids as $id) {
+            // delete template
+            $stmt = $pdo->prepare("DELETE FROM `templates`
+              WHERE id = :id AND type = :type AND NOT template = :template");
+            $stmt->execute(array(
+              ":id" => $id,
+              ":type" => "domain",
+              ":template" => "Default"
+            )); 
+          }
+
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => 'template_removed'
+          );
+          return true;
+        break;
         case 'alias':
           if (!is_array($_data['id'])) {
             $ids = array();
@@ -4518,6 +5060,42 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
           }
         break;
+        case 'mailbox_templates':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (!is_array($_data['ids'])) {
+            $ids = array();
+            $ids[] = $_data['ids'];
+          }
+          else {
+            $ids = $_data['ids'];
+          }
+
+          
+          foreach ($ids as $id) {
+            // delete template
+            $stmt = $pdo->prepare("DELETE FROM `templates`
+              WHERE id = :id AND type = :type AND NOT template = :template");
+            $stmt->execute(array(
+              ":id" => $id,
+              ":type" => "mailbox",
+              ":template" => "Default"
+            )); 
+          }
+
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => 'template_removed'
+          );
+          return true;
+        break;
         case 'resource':
           if (!is_array($_data['name'])) {
             $names = array();

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

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "25072022_2300";
+    $db_version = "16112022_1325";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -225,6 +225,22 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "templates" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "template" => "VARCHAR(255) NOT NULL",
+          "type" => "VARCHAR(255) NOT NULL",
+          "attributes" => "JSON",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("id")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "domain" => array(
         // Todo: Move some attributes to json
         "cols" => array(
@@ -1292,6 +1308,95 @@ function init_db_schema() {
     // Fix domain_admins
     $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';");
 
+    // add default templates
+    $default_domain_template = array(
+      "template" => "Default",
+      "type" => "domain",
+      "attributes" => array(
+        "tags" => array(),
+        "max_num_aliases_for_domain" => 400,
+        "max_num_mboxes_for_domain" => 10,
+        "def_quota_for_mbox" => 3072 * 1048576,
+        "max_quota_for_mbox" => 10240 * 1048576,
+        "max_quota_for_domain" => 10240 * 1048576,
+        "rl_frame" => "s",
+        "rl_value" => "",
+        "active" => 1,
+        "gal" => 1,
+        "backupmx" => 0,
+        "relay_all_recipients" => 0,
+        "relay_unknown_only" => 0,
+        "dkim_selector" => "dkim",
+        "key_size" => 2048,
+        "max_quota_for_domain" => 10240 * 1048576,
+      )
+    );     
+    $default_mailbox_template = array(
+      "template" => "Default",
+      "type" => "mailbox",
+      "attributes" => array(
+        "tags" => array(),
+        "quota" => 0,
+        "quarantine_notification" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_notification']),
+        "quarantine_category" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_category']),
+        "rl_frame" => "s",
+        "rl_value" => "",
+        "force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']),
+        "sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']),
+        "active" => 1,
+        "tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
+        "tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']),
+        "imap_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['imap_access']),
+        "pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']),
+        "smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']),
+        "sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']),
+        "acl_spam_alias" => 1,
+        "acl_tls_policy" => 1,
+        "acl_spam_score" => 1,
+        "acl_spam_policy" => 1,
+        "acl_delimiter_action" => 1,
+        "acl_syncjobs" => 0,
+        "acl_eas_reset" => 1,
+        "acl_sogo_profile_reset" => 0,
+        "acl_pushover" => 1,
+        "acl_quarantine" => 1,
+        "acl_quarantine_attachments" => 1,
+        "acl_quarantine_notification" => 1,
+        "acl_quarantine_category" => 1,
+        "acl_app_passwds" => 1,
+      )
+    );        
+    $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template");
+    $stmt->execute(array(
+      ":type" => "domain",
+      ":template" => $default_domain_template["template"]
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (empty($row)){
+      $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`)
+        VALUES (:type, :template, :attributes)");
+      $stmt->execute(array(
+        ":type" => "domain",
+        ":template" => $default_domain_template["template"],
+        ":attributes" => json_encode($default_domain_template["attributes"])
+      )); 
+    }    
+    $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template");
+    $stmt->execute(array(
+      ":type" => "mailbox",
+      ":template" => $default_mailbox_template["template"]
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (empty($row)){
+      $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`)
+        VALUES (:type, :template, :attributes)");
+      $stmt->execute(array(
+        ":type" => "mailbox",
+        ":template" => $default_mailbox_template["template"],
+        ":attributes" => json_encode($default_mailbox_template["attributes"])
+      )); 
+    } 
+
     if (php_sapi_name() == "cli") {
       echo "DB initialization completed" . PHP_EOL;
     } else {

+ 33 - 90
data/web/js/build/014-mailcow.js

@@ -289,37 +289,6 @@ $(document).ready(function() {
       addTag(this);
     } 
   });
-  function addTag(tagAddElem){
-    var tagboxElem = $(tagAddElem).parent();
-    var tagInputElem = $(tagboxElem).find(".tag-input")[0];
-    var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
-
-    var tag = escapeHtml($(tagInputElem).val());
-    if (!tag) return;
-    var value_tags = [];
-    try {
-      value_tags = JSON.parse($(tagValuesElem).val());
-    } catch {}
-    if (!Array.isArray(value_tags)) value_tags = [];
-    if (value_tags.includes(tag)) return;
-
-    $('<span class="badge bg-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + tag + '</span>').insertBefore('.tag-input').click(function(){
-      var del_tag = unescapeHtml($(this).text());
-      var del_tags = [];
-      try {
-        del_tags = JSON.parse($(tagValuesElem).val());
-      } catch {}
-      if (Array.isArray(del_tags)){
-        del_tags.splice(del_tags.indexOf(del_tag), 1);
-        $(tagValuesElem).val(JSON.stringify(del_tags));
-      }
-      $(this).remove();
-    });
-
-    value_tags.push($(tagInputElem).val());
-    $(tagValuesElem).val(JSON.stringify(value_tags));
-    $(tagInputElem).val('');
-  }
 
   // Dark Mode Loader
   $('#dark-mode-toggle').click(toggleDarkMode);
@@ -343,68 +312,42 @@ $(document).ready(function() {
       localStorage.setItem('darkmode', 'true');
     }
   }
-
-  // show whats new modal
-  if (mailcow_cc_role === "admin" || mailcow_cc_role === "domainadmin"){
-    if (mailcow_info.updatedAt > last_login){
-      var parsedSeenTimestamp = parseInt(localStorage.getItem("seenChangelog"));
-      if (!isNaN(parsedSeenTimestamp) && mailcow_info.updatedAt < parsedSeenTimestamp) {
-        console.log("changelog seen");
-        return;
-      }
-      $.ajax({
-        type: 'GET',
-        url: 'https://api.github.com/repos/' + mailcow_info.project_owner + '/' + mailcow_info.project_repo + '/releases/tags/' + mailcow_info.version_tag,
-        dataType: 'json',
-        success: function (data) { 
-          var md = window.markdownit();
-          var result = md.render(data.body);
-          result = parseGithubMarkdownLinks(result);
-
-          $('#showWhatsNewModal').find(".modal-body").html(`
-            <h3>` + data.name + `</h3>
-            <span class="mt-4">` + result + `</span>
-          `);
-
-          localStorage.setItem("seenChangelog", Math.floor(Date.now() / 1000).toString());
-        }
-      });
-
-      new bootstrap.Modal(document.getElementById("showWhatsNewModal"), {
-        backdrop: 'static',
-        keyboard: false
-      }).show();
-    }
-  }
-
-  function parseGithubMarkdownLinks(inputText) {
-    var replacedText, replacePattern1;
-  
-    replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
-    replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
-        if (matched.includes('github.com')){
-          // return short link if it's github link
-          last_uri_path = matched.split('/');
-          last_uri_path = last_uri_path[last_uri_path.length - 1];
-
-          // adjust Full Changelog link to match last git version and new git version, if link is a compare link
-          if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
-            matched = matched.replace(last_uri_path,  mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
-            last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
-          }
-          
-          return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
-        };
-  
-        // if it's not a github link, return complete link
-        return '<a href="' + matched + '" target="_blank">' + matched + '</a>'; 
-    });
-  
-    return replacedText;
-  }
 });
 
 
 // https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
 function escapeHtml(n){var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
 function unescapeHtml(t){var n={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&#x2F;":"/","&#x60;":"`","&#x3D;":"="};return String(t).replace(/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F|&#x60|&#x3D;/g,function(t){return n[t]})}
+
+function addTag(tagAddElem, tag = null){
+  var tagboxElem = $(tagAddElem).parent();
+  var tagInputElem = $(tagboxElem).find(".tag-input")[0];
+  var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
+
+  if (!tag)
+    tag = $(tagInputElem).val();
+  if (!tag) return;
+  var value_tags = [];
+  try {
+    value_tags = JSON.parse($(tagValuesElem).val());
+  } catch {}
+  if (!Array.isArray(value_tags)) value_tags = [];
+  if (value_tags.includes(tag)) return;
+
+  $('<span class="badge bg-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(tag) + '</span>').insertBefore('.tag-input').click(function(){
+    var del_tag = unescapeHtml($(this).text());
+    var del_tags = [];
+    try {
+      del_tags = JSON.parse($(tagValuesElem).val());
+    } catch {}
+    if (Array.isArray(del_tags)){
+      del_tags.splice(del_tags.indexOf(del_tag), 1);
+      $(tagValuesElem).val(JSON.stringify(del_tags));
+    }
+    $(this).remove();
+  });
+
+  value_tags.push(tag);
+  $(tagValuesElem).val(JSON.stringify(value_tags));
+  $(tagInputElem).val('');
+}

+ 68 - 7
data/web/js/site/debug.js

@@ -47,6 +47,12 @@ $(document).ready(function() {
   if (mailcow_info.branch === "master"){
     check_update(mailcow_info.version_tag, mailcow_info.project_url);
   }
+  $("#maiclow_version").click(function(){
+    if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
+      return;
+
+    showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
+  })
   // get public ips
   get_public_ips();
   update_container_stats();
@@ -1227,11 +1233,11 @@ function get_public_ips(){
   }).then(function(data) {
     console.log(data);
 
-    if (data){
-      // display host ips
+    // display host ips
+    if (data.ipv4)
       $("#host_ipv4").text(data.ipv4);
+    if (data.ipv6)
       $("#host_ipv6").text(data.ipv6);
-    }
   });
 }
 // format hosts uptime seconds to readable string
@@ -1452,10 +1458,13 @@ function check_update(current_version, github_repo_url){
       } else {
         // update available
         $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
-        $("#mailcow_update").html(
-          `<b>` + lang_debug.update_available + `
-          <a target="_blank" href="https://github.com/`+github_account+`/`+github_repo_name+`/releases/tag/`+latest_data.tag_name+`">`+latest_data.tag_name+`</a></b>`
-        );
+        $("#mailcow_update").html(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+latest_data.tag_name+`</a>`);
+        $("#mailcow_update_changelog").click(function(){
+          if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
+            return;
+      
+          showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name);
+        })
       }
     }).catch(err => {
       // err
@@ -1470,3 +1479,55 @@ function check_update(current_version, github_repo_url){
     $("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
   });
 }
+// show version changelog modal
+function showVersionModal(title, version){
+  $.ajax({
+    type: 'GET',
+    url: 'https://api.github.com/repos/' + mailcow_info.project_owner + '/' + mailcow_info.project_repo + '/releases/tags/' + version,
+    dataType: 'json',
+    success: function (data) { 
+      var md = window.markdownit();
+      var result = md.render(data.body);
+      result = parseGithubMarkdownLinks(result);
+
+      $('#showVersionModal').find(".modal-title").html(title);
+      $('#showVersionModal').find(".modal-body").html(`
+        <h3>` + data.name + `</h3>
+        <span class="mt-4">` + result + `</span>
+        <span><b>Github Link:</b> 
+          <a target="_blank" href="https://github.com/` + mailcow_info.project_owner + `/` + mailcow_info.project_repo + `/releases/tag/` + version + `">` + version + `</a>
+        </span>
+      `);
+
+      new bootstrap.Modal(document.getElementById("showVersionModal"), {
+        backdrop: 'static',
+        keyboard: false
+      }).show();
+    }
+  });
+}
+function parseGithubMarkdownLinks(inputText) {
+  var replacedText, replacePattern1;
+
+  replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
+  replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
+      if (matched.includes('github.com')){
+        // return short link if it's github link
+        last_uri_path = matched.split('/');
+        last_uri_path = last_uri_path[last_uri_path.length - 1];
+
+        // adjust Full Changelog link to match last git version and new git version, if link is a compare link
+        if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
+          matched = matched.replace(last_uri_path,  mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
+          last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
+        }
+        
+        return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
+      };
+
+      // if it's not a github link, return complete link
+      return '<a href="' + matched + '" target="_blank">' + matched + '</a>'; 
+  });
+
+  return replacedText;
+}

+ 11 - 0
data/web/js/site/edit.js

@@ -57,6 +57,17 @@ $(document).ready(function() {
   $("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
     $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
   });
+
+  // load tags
+  if ($('#tags').length){
+    var tagsEl = $('#tags').parent().find('.tag-values')[0];
+    console.log($(tagsEl).val())
+    var tags = JSON.parse($(tagsEl).val());
+    $(tagsEl).val("");
+    
+    for (var i = 0; i < tags.length; i++)
+      addTag($('#tags'), tags[i]);
+  }
 });
 
 jQuery(function($){

+ 700 - 0
data/web/js/site/mailbox.js

@@ -77,6 +77,90 @@ $(document).ready(function() {
         $('.dns-modal-body').html(xhr.responseText);
       }
     });
+  }); 
+  // @Open Domain add modal
+  $('#addDomainModal').on('show.bs.modal', function(e) {
+    $.ajax({
+      url: '/api/v1/get/domain/template/all',
+      data: {},
+      dataType: 'json',
+      success: async function(data){
+        $('#domain_templates').find('option').remove(); 
+        $('#domain_templates').selectpicker('destroy');
+        $('#domain_templates').selectpicker();
+        for (var i = 0; i < data.length; i++){
+          if (data[i].template === "Default"){
+            $('#domain_templates').prepend($('<option>', { 
+                'value': data[i].id,
+                'text': data[i].template,
+                'data-attributes': JSON.stringify(data[i].attributes),
+                'selected': true
+            }));
+            setDomainTemplateData(data[i].attributes);
+          } else {
+            $('#domain_templates').append($('<option>', { 
+                'value': data[i].id,
+                'text': data[i].template,
+                'data-attributes': JSON.stringify(data[i].attributes),
+                'selected': false
+            }));
+          }
+        };
+        $('#domain_templates').selectpicker("refresh");
+
+        // @selecting template
+        $('#domain_templates').on('change', function(){
+          var selected = $('#domain_templates option:selected');
+          var attr = selected.data('attributes');
+          setDomainTemplateData(attr);
+        });
+      },
+      error: function(xhr, status, error) {
+        console.log(error);
+      }
+    });
+  });
+  // @Open Mailbox add modal
+  $('#addMailboxModal').on('show.bs.modal', function(e) {
+    $.ajax({
+      url: '/api/v1/get/mailbox/template/all',
+      data: {},
+      dataType: 'json',
+      success: async function(data){
+        $('#mailbox_templates').find('option').remove(); 
+        $('#mailbox_templates').selectpicker('destroy');
+        $('#mailbox_templates').selectpicker();
+        for (var i = 0; i < data.length; i++){
+          if (data[i].template === "Default"){
+            $('#mailbox_templates').prepend($('<option>', { 
+                'value': data[i].id,
+                'text': data[i].template,
+                'data-attributes': JSON.stringify(data[i].attributes),
+                'selected': true
+            }));
+            setMailboxTemplateData(data[i].attributes);
+          } else {
+            $('#mailbox_templates').append($('<option>', { 
+                value: data[i].id,
+                text : data[i].template,
+                'data-attributes': JSON.stringify(data[i].attributes),
+                'selected': false
+            }));
+          }
+        };
+        $('#mailbox_templates').selectpicker("refresh");
+
+        // @selecting template
+        $('#mailbox_templates').on('change', function(){
+          var selected = $('#mailbox_templates option:selected');
+          var attr = selected.data('attributes');
+          setMailboxTemplateData(attr);
+        });
+      },
+      error: function(xhr, status, error) {
+        console.log(error);
+      }
+    });
   });
   // Sieve data modal
   $('#sieveDataModal').on('show.bs.modal', function(e) {
@@ -133,6 +217,201 @@ $(document).ready(function() {
     $("#multiple_bookings").val($("#multiple_bookings_custom").val());
   });
 
+  function setDomainTemplateData(template){
+    $("#addDomain_max_aliases").val(template.max_num_aliases_for_domain);
+    $("#addDomain_max_mailboxes").val(template.max_num_mboxes_for_domain);
+    $("#addDomain_mailbox_quota_def").val(template.def_quota_for_mbox / 1048576);
+    $("#addDomain_mailbox_quota_m").val(template.max_quota_for_mbox / 1048576);
+    $("#addDomain_domain_quota_m").val(template.max_quota_for_domain / 1048576);
+
+    if (template.gal == 1){
+      $('#addDomain_gal').prop('checked', true);
+    } else {
+      $('#addDomain_gal').prop('checked', false);
+    }
+    
+    if (template.active == 1){
+      $('#addDomain_active').prop('checked', true);
+    } else {
+      $('#addDomain_active').prop('checked', false);
+    }
+    
+    $("#addDomain_rl_value").val(template.rl_value);
+    $('#addDomain_rl_frame').selectpicker('val', template.rl_frame);
+    $("#dkim_selector").val(template.dkim_selector);
+    if (!template.key_size)
+      template.key_size = 2048;
+    $('#key_size').selectpicker('val', template.key_size.toString());
+    
+    if (template.backupmx == 1){
+      $('#addDomain_relay_domain').prop('checked', true);
+    } else {
+      $('#addDomain_relay_domain').prop('checked', false);
+    }
+    if (template.relay_all_recipients == 1){
+      $('#addDomain_relay_all').prop('checked', true);
+    } else {
+      $('#addDomain_relay_all').prop('checked', false);
+    }
+    if (template.relay_unknown_only == 1){
+      $('#addDomain_relay_unknown_only').prop('checked', true);
+    } else {
+      $('#addDomain_relay_unknown_only').prop('checked', false);
+    }
+
+    
+    // load tags
+    $('#addDomain_tags').val("");
+    $($('#addDomain_tags').parent().find(".tag-values")[0]).val("");
+    $('#addDomain_tags').parent().find(".tag-badge").remove();
+    for (var i = 0; i < template.tags.length; i++)
+      addTag($('#addDomain_tags'), template.tags[i]);
+  }
+  function setMailboxTemplateData(template){
+    $("#addInputQuota").val(template.quota / 1048576);
+
+    if (template.quarantine_notification === "never"){
+      $('#quarantine_notification_never').prop('checked', true);
+      $('#quarantine_notification_hourly').prop('checked', false);
+      $('#quarantine_notification_daily').prop('checked', false);
+      $('#quarantine_notification_weekly').prop('checked', false);
+    } else if(template.quarantine_notification === "hourly"){
+      $('#quarantine_notification_never').prop('checked', false);
+      $('#quarantine_notification_hourly').prop('checked', true);
+      $('#quarantine_notification_daily').prop('checked', false);
+      $('#quarantine_notification_weekly').prop('checked', false);
+    } else if(template.quarantine_notification === "daily"){
+      $('#quarantine_notification_never').prop('checked', false);
+      $('#quarantine_notification_hourly').prop('checked', false);
+      $('#quarantine_notification_daily').prop('checked', true);
+      $('#quarantine_notification_weekly').prop('checked', false);
+    } else if(template.quarantine_notification === "weekly"){
+      $('#quarantine_notification_never').prop('checked', false);
+      $('#quarantine_notification_hourly').prop('checked', false);
+      $('#quarantine_notification_daily').prop('checked', false);
+      $('#quarantine_notification_weekly').prop('checked', true);
+    } else {
+      $('#quarantine_notification_never').prop('checked', false);
+      $('#quarantine_notification_hourly').prop('checked', false);
+      $('#quarantine_notification_daily').prop('checked', false);
+      $('#quarantine_notification_weekly').prop('checked', false);
+    }
+
+    if (template.quarantine_category === "reject"){
+      $('#quarantine_category_reject').prop('checked', true);
+      $('#quarantine_category_add_header').prop('checked', false);
+      $('#quarantine_category_all').prop('checked', false);
+    } else if(template.quarantine_category === "add_header"){
+      $('#quarantine_category_reject').prop('checked', false);
+      $('#quarantine_category_add_header').prop('checked', true);
+      $('#quarantine_category_all').prop('checked', false);
+    } else if(template.quarantine_category === "all"){
+      $('#quarantine_category_reject').prop('checked', false);
+      $('#quarantine_category_add_header').prop('checked', false);
+      $('#quarantine_category_all').prop('checked', true);
+    }
+
+    if (template.tls_enforce_in == 1){
+      $('#tls_enforce_in').prop('checked', true);
+    } else {
+      $('#tls_enforce_in').prop('checked', false);
+    }
+    if (template.tls_enforce_out == 1){
+      $('#tls_enforce_out').prop('checked', true);
+    } else {
+      $('#tls_enforce_out').prop('checked', false);
+    }
+
+    var protocol_access = [];
+    if (template.imap_access == 1){
+      protocol_access.push("imap");
+    }
+    if (template.pop3_access == 1){
+      protocol_access.push("pop3");
+    }
+    if (template.smtp_access == 1){
+      protocol_access.push("smtp");
+    }
+    if (template.sieve_access == 1){
+      protocol_access.push("sieve");
+    }
+    $('#protocol_access').selectpicker('val', protocol_access);
+
+    var acl = [];
+    if (template.acl_spam_alias == 1){
+      acl.push("spam_alias");
+    }
+    if (template.acl_tls_policy == 1){
+      acl.push("tls_policy");
+    }
+    if (template.acl_spam_score == 1){
+      acl.push("spam_score");
+    }
+    if (template.acl_spam_policy == 1){
+      acl.push("spam_policy");
+    }
+    if (template.acl_delimiter_action == 1){
+      acl.push("delimiter_action");
+    }
+    if (template.acl_syncjobs == 1){
+      acl.push("syncjobs");
+    }
+    if (template.acl_eas_reset == 1){
+      acl.push("eas_reset");
+    }
+    if (template.acl_sogo_profile_reset == 1){
+      acl.push("sogo_profile_reset");
+    }
+    if (template.acl_pushover == 1){
+      acl.push("pushover");
+    }
+    if (template.acl_quarantine == 1){
+      acl.push("quarantine");
+    }
+    if (template.acl_quarantine_attachments == 1){
+      acl.push("quarantine_attachments");
+    }
+    if (template.acl_quarantine_notification == 1){
+      acl.push("quarantine_notification");
+    }
+    if (template.acl_quarantine_category == 1){
+      acl.push("quarantine_category");
+    }
+    if (template.acl_app_passwds == 1){
+      acl.push("app_passwds");
+    }
+    $('#user_acl').selectpicker('val', acl);
+
+    $('#rl_value').val(template.rl_value);
+    if (template.rl_frame){
+      $('#rl_frame').selectpicker('val', template.rl_frame);
+    }
+
+    console.log(template.active)
+    if (template.active){
+      $('#mbox_active').selectpicker('val', template.active.toString());
+    } else {
+      $('#mbox_active').selectpicker('val', '');
+    }
+
+    if (template.force_pw_update == 1){
+      $('#force_pw_update').prop('checked', true);
+    } else {
+      $('#force_pw_update').prop('checked', false);
+    }
+    if (template.sogo_access == 1){
+      $('#sogo_access').prop('checked', true);
+    } else {
+      $('#sogo_access').prop('checked', false);
+    }
+    
+    // load tags
+    $('#addMailbox_tags').val("");
+    $($('#addMailbox_tags').parent().find(".tag-values")[0]).val("");
+    $('#addMailbox_tags').parent().find(".tag-badge").remove();
+    for (var i = 0; i < template.tags.length; i++)
+      addTag($('#addMailbox_tags'), template.tags[i]);
+  }
 });
 jQuery(function($){
   // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
@@ -293,6 +572,18 @@ jQuery(function($){
           defaultContent: '',
           className: 'none'
         },
+        {
+          title: lang.created_on,
+          data: 'created',
+          defaultContent: '',
+          className: 'none'
+        },
+        {
+          title: lang.last_modified,
+          data: 'modified',
+          defaultContent: '',
+          className: 'none'
+        },
         {
           title: 'Tags',
           data: 'tags',
@@ -318,6 +609,196 @@ jQuery(function($){
       ]
     });  
   }
+  function draw_templates_domain_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#templates_domain_table') ) {
+      $('#templates_domain_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#templates_domain_table').DataTable({
+			responsive : true,
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/domain/template/all",
+        dataSrc: function(json){
+          console.log(json);
+          $.each(json, function (i, item) {
+            item.chkbox = '<input type="checkbox" data-id="domain_template" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
+
+            item.attributes.def_quota_for_mbox = humanFileSize(item.attributes.def_quota_for_mbox);
+            item.attributes.max_quota_for_mbox = humanFileSize(item.attributes.max_quota_for_mbox);
+            item.attributes.max_quota_for_domain = humanFileSize(item.attributes.max_quota_for_domain);
+
+            item.template = escapeHtml(item.template);
+            if (item.attributes.rl_frame === "s"){
+              item.attributes.rl_frame = lang_rl.second;
+            } else if (item.attributes.rl_frame === "m"){
+              item.attributes.rl_frame = lang_rl.minute;
+            } else if (item.attributes.rl_frame === "h"){
+              item.attributes.rl_frame = lang_rl.hour;
+            } else if (item.attributes.rl_frame === "d"){
+              item.attributes.rl_frame = lang_rl.day;
+            }
+            item.attributes.rl_value = escapeHtml(item.attributes.rl_value);
+
+            item.action = '<div class="btn-group">' +
+              '<a href="/edit/template/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-template" data-api-url="delete/domain/template" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+              '</div>';
+
+            if (Array.isArray(item.attributes.tags)){
+              var tags = '';
+              for (var i = 0; i < item.attributes.tags.length; i++)
+                tags += '<span class="badge bg-primary tag-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(item.attributes.tags[i]) + '</span>';
+              item.attributes.tags = tags;
+            } else {
+              item.attributes.tags = '';
+            }
+          });
+
+          return json;
+        }
+      },
+      columns: [
+          {
+            // placeholder, so checkbox will not block child row toggle
+            title: '',
+            data: null,
+            searchable: false,
+            orderable: false,
+            defaultContent: '',
+            responsivePriority: 1
+          },
+          {
+            title: '',
+            data: 'chkbox',
+            searchable: false,
+            orderable: false,
+            defaultContent: '',
+            responsivePriority: 1
+          },
+          {
+            title: "ID",
+            data: 'id',
+            responsivePriority: 2,
+            defaultContent: ''
+          },
+          {
+            title: "Template",
+            data: 'template',
+            responsivePriority: 3,
+            defaultContent: ''
+          },              
+          {
+            title: lang.max_aliases,
+            data: 'attributes.max_num_aliases_for_domain',
+            defaultContent: '',
+          },             
+          {
+            title: lang.max_mailboxes,
+            data: 'attributes.max_num_mboxes_for_domain',
+            defaultContent: '',
+          },             
+          {
+            title: lang.mailbox_defquota,
+            data: 'attributes.def_quota_for_mbox',
+            defaultContent: '',
+          },               
+          {
+            title: lang.max_quota,
+            data: 'attributes.max_quota_for_mbox',
+            defaultContent: '',
+          },            
+          {
+            title: lang.domain_quota_total,
+            data: 'attributes.max_quota_for_domain',
+            defaultContent: '',
+          },          
+          {
+            title: lang.gal,
+            data: 'attributes.gal',
+            defaultContent: '',
+            render: function (data, type) {
+              return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
+            }
+          },           
+          {
+            title: lang.backup_mx,
+            data: 'attributes.backupmx',
+            defaultContent: '',
+            render: function (data, type) {
+              return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
+            }
+          },           
+          {
+            title: lang.relay_all,
+            data: 'attributes.relay_all_recipients',
+            defaultContent: '',
+            render: function (data, type) {
+              return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
+            }
+          },           
+          {
+            title: lang.relay_unknown,
+            data: 'attributes.relay_unknown_only',
+            defaultContent: '',
+            render: function (data, type) {
+              return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
+            }
+          },           
+          {
+            title: lang.active,
+            data: 'attributes.active',
+            defaultContent: '',
+            responsivePriority: 4,
+            render: function (data, type) {
+              return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
+            }
+          },               
+          {
+            title: 'rl_frame',
+            data: 'attributes.rl_frame',
+            defaultContent: '',
+            class: 'none',
+          },             
+          {
+            title: 'rl_value',
+            data: 'attributes.rl_value',
+            defaultContent: '',
+            class: 'none',
+          },            
+          {
+            title: lang.dkim_domains_selector,
+            data: 'attributes.dkim_selector',
+            defaultContent: '',
+            class: 'none',
+          },            
+          {
+            title: lang.dkim_key_length,
+            data: 'attributes.key_size',
+            defaultContent: '',
+            class: 'none',
+          }, 
+          {
+            title: 'Tags',
+            data: 'attributes.tags',
+            defaultContent: '',
+            className: 'none'
+          },    
+          {
+            title: lang.action,
+            data: 'action',
+            className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md',
+            responsivePriority: 6,
+            defaultContent: ''
+          },
+      ]
+    });
+  }
   function draw_mailbox_table() {
     // just recalc width if instance already exists
     if ($.fn.DataTable.isDataTable('#mailbox_table') ) {
@@ -539,6 +1020,18 @@ jQuery(function($){
             defaultContent: '',
             responsivePriority: 5
           },
+          {
+            title: lang.created_on,
+            data: 'created',
+            defaultContent: '',
+            className: 'none'
+          },
+          {
+            title: lang.last_modified,
+            data: 'modified',
+            defaultContent: '',
+            className: 'none'
+          },
           {
             title: 'Tags',
             data: 'tags',
@@ -564,6 +1057,211 @@ jQuery(function($){
       ]
     });
   }
+  function draw_templates_mbox_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#templates_mbox_table') ) {
+      $('#templates_mbox_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#templates_mbox_table').DataTable({
+			responsive : true,
+      processing: true,
+      serverSide: false,
+      language: lang_datatables,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/mailbox/template/all",
+        dataSrc: function(json){
+          $.each(json, function (i, item) {
+            item.chkbox = '<input type="checkbox" data-id="mailbox_template" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
+
+            item.template = escapeHtml(item.template);
+            if (item.attributes.rl_frame === "s"){
+              item.attributes.rl_frame = lang_rl.second;
+            } else if (item.attributes.rl_frame === "m"){
+              item.attributes.rl_frame = lang_rl.minute;
+            } else if (item.attributes.rl_frame === "h"){
+              item.attributes.rl_frame = lang_rl.hour;
+            } else if (item.attributes.rl_frame === "d"){
+              item.attributes.rl_frame = lang_rl.day;
+            }
+            item.attributes.rl_value = escapeHtml(item.attributes.rl_value);
+
+            item.attributes.quota = humanFileSize(item.attributes.quota);
+
+            item.attributes.tls_enforce_in = '<i class="text-' + (item.attributes.tls_enforce_in == 1 ? 'success bi bi-lock-fill' : 'danger bi bi-unlock-fill') + '"></i>';
+            item.attributes.tls_enforce_out = '<i class="text-' + (item.attributes.tls_enforce_out == 1 ? 'success bi bi-lock-fill' : 'danger bi bi-unlock-fill') + '"></i>';
+            item.attributes.pop3_access = '<i class="text-' + (item.attributes.pop3_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.pop3_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
+            item.attributes.imap_access = '<i class="text-' + (item.attributes.imap_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.imap_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
+            item.attributes.smtp_access = '<i class="text-' + (item.attributes.smtp_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.smtp_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
+            item.attributes.sieve_access = '<i class="text-' + (item.attributes.sieve_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sieve_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
+            item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"></i>';
+            if (item.attributes.quarantine_notification === 'never') {
+              item.attributes.quarantine_notification = lang.never;
+            } else if (item.attributes.quarantine_notification === 'hourly') {
+              item.attributes.quarantine_notification = lang.hourly;
+            } else if (item.attributes.quarantine_notification === 'daily') {
+              item.attributes.quarantine_notification = lang.daily;
+            } else if (item.attributes.quarantine_notification === 'weekly') {
+              item.attributes.quarantine_notification = lang.weekly;
+            }
+            if (item.attributes.quarantine_category === 'reject') {
+              item.attributes.quarantine_category = '<span class="text-danger">' + lang.q_reject + '</span>';
+            } else if (item.attributes.quarantine_category === 'add_header') {
+              item.attributes.quarantine_category = '<span class="text-warning">' + lang.q_add_header + '</span>';
+            } else if (item.attributes.quarantine_category === 'all') {
+              item.attributes.quarantine_category = lang.q_all;
+            }
+
+            
+
+            item.action = '<div class="btn-group">' +
+              '<a href="/edit/template/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-template" data-api-url="delete/mailbox/template" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+              '</div>';
+
+            if (Array.isArray(item.attributes.tags)){
+              var tags = '';
+              for (var i = 0; i < item.attributes.tags.length; i++)
+                tags += '<span class="badge bg-primary tag-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(item.attributes.tags[i]) + '</span>';
+              item.attributes.tags = tags;
+            } else {
+              item.attributes.tags = '';
+            }
+          });
+
+          return json;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: '',
+          responsivePriority: 1
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: '',
+          responsivePriority: 1
+        },
+        {
+          title: "ID",
+          data: 'id',
+          responsivePriority: 2,
+          defaultContent: ''
+        },
+        {
+          title: "Template",
+          data: 'template',
+          responsivePriority: 3,
+          defaultContent: ''
+        },              
+        {
+          title: lang.domain_quota,
+          data: 'attributes.quota',
+          defaultContent: '',
+        },             
+        {
+          title: lang.tls_enforce_in,
+          data: 'attributes.tls_enforce_in',
+          defaultContent: ''
+        },
+        {
+          title: lang.tls_enforce_out,
+          data: 'attributes.tls_enforce_out',
+          defaultContent: ''
+        },
+        {
+          title: 'SMTP',
+          data: 'attributes.smtp_access',
+          defaultContent: '',
+        },
+        {
+          title: 'IMAP',
+          data: 'attributes.imap_access',
+          defaultContent: '',
+        },
+        {
+          title: 'POP3',
+          data: 'attributes.pop3_access',
+          defaultContent: '',
+        },
+        {
+          title: 'SIEVE',
+          data: 'attributes.sieve_access',
+          defaultContent: '',
+        },
+        {
+          title: 'SOGO',
+          data: 'attributes.sogo_access',
+          defaultContent: '',
+        },
+        {
+          title: lang.quarantine_notification,
+          data: 'attributes.quarantine_notification',
+          defaultContent: '',
+          className: 'none'
+        },
+        {
+          title: lang.quarantine_category,
+          data: 'attributes.quarantine_category',
+          defaultContent: '',
+          className: 'none'
+        },            
+        {
+          title: lang.force_pw_update,
+          data: 'attributes.force_pw_update',
+          defaultContent: '',
+          class: 'none',
+          render: function (data, type) {
+            return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
+          }
+        },            
+        {
+          title: "rl_frame",
+          data: 'attributes.rl_frame',
+          defaultContent: '',
+          class: 'none',
+        },           
+        {
+          title: 'rl_value',
+          data: 'attributes.rl_value',
+          defaultContent: '',
+          class: 'none',
+        }, 
+        {
+          title: 'Tags',
+          data: 'attributes.tags',
+          defaultContent: '',
+          className: 'none'
+        },           
+        {
+          title: lang.active,
+          data: 'attributes.active',
+          defaultContent: '',
+          responsivePriority: 4,
+          render: function (data, type) {
+            return 1==data?'<i class="bi bi-check-lg"></i>':(0==data?'<i class="bi bi-x-lg"></i>':2==data&&'&#8212;');
+          }
+        },     
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md',
+          responsivePriority: 6,
+          defaultContent: ''
+        },
+      ]
+    });
+  }
   function draw_resource_table() {
     // just recalc width if instance already exists
     if ($.fn.DataTable.isDataTable('#resource_table') ) {
@@ -1437,7 +2135,9 @@ jQuery(function($){
 
   // Load only if the tab is visible
   onVisible("[id^=domain_table]", () => draw_domain_table());
+  onVisible("[id^=templates_domain_table]", () => draw_templates_domain_table());
   onVisible("[id^=mailbox_table]", () => draw_mailbox_table());
+  onVisible("[id^=templates_mbox_table]", () => draw_templates_mbox_table());
   onVisible("[id^=resource_table]", () => draw_resource_table());
   onVisible("[id^=alias_table]", () => draw_alias_table());
   onVisible("[id^=aliasdomain_table]", () => draw_aliasdomain_table());

+ 58 - 6
data/web/json_api.php

@@ -230,13 +230,27 @@ if (isset($_GET['query'])) {
           process_add_return(rsettings('add', $attr));
         break;
         case "mailbox":
-          process_add_return(mailbox('add', 'mailbox', $attr));
+          switch ($object) {
+            case "template":
+              process_add_return(mailbox('add', 'mailbox_templates', $attr));
+            break;
+            default:
+              process_add_return(mailbox('add', 'mailbox', $attr));
+            break;
+          }
         break;
         case "oauth2-client":
           process_add_return(oauth2('add', 'client', $attr));
         break;
         case "domain":
-          process_add_return(mailbox('add', 'domain', $attr));
+          switch ($object) {
+            case "template":
+              process_add_return(mailbox('add', 'domain_templates', $attr));
+            break;
+            default:
+              process_add_return(mailbox('add', 'domain', $attr));
+            break;
+          }  
         break;
         case "resource":
           process_add_return(mailbox('add', 'resource', $attr));
@@ -519,7 +533,16 @@ if (isset($_GET['query'])) {
                   echo '{}';
                 }
               break;
-
+              case "template":
+                switch ($extra){
+                  case "all":
+                    process_get_return(mailbox('get', 'domain_templates'));
+                  break;
+                  default:
+                    process_get_return(mailbox('get', 'domain_templates', $extra));
+                  break;
+                }
+              break;
               default:
                 $data = mailbox('get', 'domain_details', $object);
                 process_get_return($data);
@@ -992,7 +1015,16 @@ if (isset($_GET['query'])) {
                   echo '{}';
                 }
               break;
-
+              case "template":
+                switch ($extra){
+                  case "all":
+                    process_get_return(mailbox('get', 'mailbox_templates'));
+                  break;
+                  default:
+                    process_get_return(mailbox('get', 'mailbox_templates', $extra));
+                  break;
+                }
+              break;
               default:
                 $tags = null;
                 if (isset($_GET['tags']) && $_GET['tags'] != '') 
@@ -1641,6 +1673,9 @@ if (isset($_GET['query'])) {
             case "tag":
               process_delete_return(mailbox('delete', 'tags_domain', array('tags' => $items, 'domain' => $extra)));
             break;
+            case "template":
+              process_delete_return(mailbox('delete', 'domain_templates', array('ids' => $items)));
+            break;
             default:
               process_delete_return(mailbox('delete', 'domain', array('domain' => $items)));
           }
@@ -1653,6 +1688,9 @@ if (isset($_GET['query'])) {
             case "tag":
               process_delete_return(mailbox('delete', 'tags_mailbox', array('tags' => $items, 'username' => $extra)));
             break;
+            case "template":
+              process_delete_return(mailbox('delete', 'mailbox_templates', array('ids' => $items)));
+            break;
             default:
               process_delete_return(mailbox('delete', 'mailbox', array('username' => $items)));
           }
@@ -1814,7 +1852,14 @@ if (isset($_GET['query'])) {
           process_edit_return(mailbox('edit', 'time_limited_alias', array_merge(array('address' => $items), $attr)));
         break;
         case "mailbox":
-          process_edit_return(mailbox('edit', 'mailbox', array_merge(array('username' => $items), $attr)));
+          switch ($object) {
+            case "template":
+              process_edit_return(mailbox('edit', 'mailbox_templates', array_merge(array('ids' => $items), $attr)));
+            break;
+            default:
+              process_edit_return(mailbox('edit', 'mailbox', array_merge(array('username' => $items), $attr)));
+            break;
+          }
         break;
         case "syncjob":
           process_edit_return(mailbox('edit', 'syncjob', array_merge(array('id' => $items), $attr)));
@@ -1826,7 +1871,14 @@ if (isset($_GET['query'])) {
           process_edit_return(mailbox('edit', 'resource', array_merge(array('name' => $items), $attr)));
         break;
         case "domain":
-          process_edit_return(mailbox('edit', 'domain', array_merge(array('domain' => $items), $attr)));
+          switch ($object) {
+            case "template":
+              process_edit_return(mailbox('edit', 'domain_templates', array_merge(array('ids' => $items), $attr)));
+            break;
+            default:
+              process_edit_return(mailbox('edit', 'domain', array_merge(array('domain' => $items), $attr)));
+            break;
+          }
         break;
         case "rl-domain":
           process_edit_return(ratelimit('edit', 'domain', array_merge(array('object' => $items), $attr)));

+ 6 - 0
data/web/lang/lang.ca-es.json

@@ -316,6 +316,7 @@
         "bcc_type": "BCC type",
         "deactivate": "Desactivar",
         "description": "Descripció",
+        "dkim_key_length": "Mida de la clau DKIM (bits)",
         "domain": "Domini",
         "domain_admins": "Administradores de dominio",
         "domain_aliases": "Àlies de domini",
@@ -327,6 +328,7 @@
         "filter_table": "Filtrar taula",
         "filters": "Filtres",
         "fname": "Nom complert",
+        "force_pw_update": "Forçar l'actualització de la contrassenya al proper login",
         "in_use": "En ús (%)",
         "inactive": "Inactiu",
         "kind": "Tipus",
@@ -334,6 +336,9 @@
         "last_run_reset": "Executar a continuació",
         "mailbox_quota": "Mida màx. de quota",
         "mailboxes": "Bústies",
+        "max_aliases": "Màx. àlies possibles",
+        "max_mailboxes": "Màx. bústies possibles",
+        "max_quota": "Màx. quota per bústia",
         "mins_interval": "Intèrval (min)",
         "msg_num": "Missatge #",
         "multiple_bookings": "Múltiples reserves",
@@ -346,6 +351,7 @@
         "recipient_map_new": "Nou destinatari",
         "recipient_map_old": "Destinatari original",
         "recipient_maps": "Recipient maps",
+        "relay_all": "Retransmetre tods els recipients",
         "remove": "Esborrar",
         "resources": "Recursos",
         "running": "Executant-se",

+ 13 - 0
data/web/lang/lang.cs-cz.json

@@ -507,6 +507,7 @@
         "bcc_dest_format": "Cíl kopie musí být jedna platná email adresa. Pokud potřebujete posílat kopie na více adres, vytvořte Alias a použijte jej zde.",
         "client_id": "ID klienta",
         "client_secret": "Tajný klíč klienta",
+        "created_on": "Vytvoreno",
         "comment_info": "Soukromý komentář se nezobrazí uživateli; veřejný komentář se zobrazí jako nápověda při zastavení se kurzorem v přehledu uživatelů",
         "delete1": "Odstranit ze zdrojové schránky, po dokončení přenosu",
         "delete2": "Odstranit zprávy v cílové schránce, pokud nejsou ve zdrojové",
@@ -534,6 +535,7 @@
         "hostname": "Jméno hostitele",
         "inactive": "Neaktivní",
         "kind": "Druh",
+        "last_modified": "Naposledy změněn",
         "lookup_mx": "Cíl je regulární výraz který se shoduje s MX záznamem (<code>.*google\\.com</code> směřuje veškerou poštu na MX které jsou cílem pro google.com přes tento skok)",
         "mailbox": "Úprava mailové schránky",
         "mailbox_quota_def": "Výchozí kvóta schránky",
@@ -701,15 +703,19 @@
         "booking_ltnull": "Neomezeno, ale po rezervaci se ukazuje jako obsazené",
         "booking_lt0_short": "Volný limit",
         "catch_all": "Doménový koš",
+        "created_on": "Vytvoreno",
         "daily": "Každý den",
         "deactivate": "Vypnout",
         "description": "Popis",
         "disable_login": "Zakázat přihlášení (ale stále přijímat poštu)",
         "disable_x": "Vypnout",
+        "dkim_domains_selector": "Selektor",
+        "dkim_key_length": "Délka DKIM klíče (v bitech)",
         "domain": "Doména",
         "domain_admins": "Správci domén",
         "domain_aliases": "Doménové aliasy",
         "domain_quota": "Kvóta",
+        "domain_quota_total": "Celková kvóta domény",
         "domains": "Domény",
         "edit": "Upravit",
         "empty": "Žádné výsledky",
@@ -718,6 +724,8 @@
         "filter_table": "Filtrovat tabulku",
         "filters": "Filtry",
         "fname": "Celé jméno",
+        "force_pw_update": "Vynutit změnu hesla při příštím přihlášení",
+        "gal": "Globální seznam adres",
         "goto_ham": "Učit se jako <b>ham</b>",
         "goto_spam": "Učit se jako <b>spam</b>",
         "hourly": "Každou hodinu",
@@ -726,6 +734,7 @@
         "insert_preset": "Vložit ukázkovou položku \"%s\"",
         "kind": "Druh",
         "last_mail_login": "Poslední přihlášení",
+        "last_modified": "Naposledy změněn",
         "last_pw_change": "Naposledy změněno heslo",
         "last_run": "Naposledy spuštěno",
         "last_run_reset": "Znovu naplánovat",
@@ -735,6 +744,9 @@
         "mailbox_defquota": "Výchozí velikost schránky",
         "mailbox_quota": "Max. velikost schránky",
         "mailboxes": "Mailové schránky",
+        "max_aliases": "Max. počet aliasů",
+        "max_mailboxes": "Max. počet mailových schránek",
+        "max_quota": "Max. kvóta mailové schránky",
         "mins_interval": "Interval (min)",
         "msg_num": "Počet zpráv",
         "multiple_bookings": "Vícenásobné rezervace",
@@ -760,6 +772,7 @@
         "recipient_map_old": "Původní příjemce",
         "recipient_map_old_info": "Původní příjemce musí být platná emailová adresa nebo název domény.",
         "recipient_maps": "Mapy příjemců",
+        "relay_all": "Předávání všech příjemců",
         "remove": "Smazat",
         "resources": "Zdroje",
         "running": "Běží",

+ 9 - 1
data/web/lang/lang.da-dk.json

@@ -640,11 +640,14 @@
         "deactivate": "Deaktiver",
         "description": "Beskrivelse",
         "disable_login": "Tillad ikke login (indgående mail accepteres stadig)",
-        "disable_x": "Deaktiver",
+        "disable_x": "Deaktiver",        
+        "dkim_domains_selector": "Vælger",
+        "dkim_key_length": "DKIM nøgle længde (bits)",
         "domain": "Domæne",
         "domain_admins": "Domæneadministratorer",
         "domain_aliases": "Domænealiaser",
         "domain_quota": "Kvote",
+        "domain_quota_total": "Samlet domænekvote",
         "domains": "Domains",
         "edit": "Edit",
         "empty": "Ingen resultater",
@@ -653,6 +656,8 @@
         "filter_table": "Filtertabel",
         "filters": "Filtre",
         "fname": "Fulde navn",
+        "force_pw_update": "Tving adgangskodeopdatering til næste login",   
+        "gal": "Global adresseliste",
         "hourly": "Hver time",
         "in_use": "I brug (%)",
         "inactive": "Inaktiv",
@@ -667,6 +672,9 @@
         "mailboxes": "Postkasser",
         "mailbox_defaults": "Standardindstillinger",
         "mailbox_defaults_info": "Definer standardindstillinger for nye postkasser.",
+        "max_aliases": "Maks. mulige aliasser",
+        "max_mailboxes": "Maks. mulige postkasser",
+        "max_quota": "Maks. kvote pr. postkasse",
         "mins_interval": "Interval (min)",
         "msg_num": "Besked #",
         "multiple_bookings": "Flere bookinger",

+ 13 - 0
data/web/lang/lang.de-de.json

@@ -547,6 +547,7 @@
         "client_id": "Client-ID",
         "client_secret": "Client-Secret",
         "comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
+        "created_on": "Erstellt am",
         "delete1": "Lösche Nachricht nach Übertragung vom Quell-Server",
         "delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind",
         "delete2duplicates": "Lösche Duplikate im Ziel",
@@ -573,6 +574,7 @@
         "hostname": "Servername",
         "inactive": "Inaktiv",
         "kind": "Art",
+        "last_modified": "Zuletzt geändert",
         "lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
         "mailbox": "Mailbox bearbeiten",
         "mailbox_quota_def": "Standard-Quota einer Mailbox",
@@ -741,15 +743,19 @@
         "booking_custom_short": "Hartes Limit",
         "booking_ltnull": "Unbegrenzt, jedoch anzeigen, wenn gebucht",
         "booking_lt0_short": "Weiches Limit",
+        "created_on": "Erstellt am",
         "daily": "Täglich",
         "deactivate": "Deaktivieren",
         "description": "Beschreibung",
         "disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
         "disable_x": "Deaktivieren",
+        "dkim_domains_selector": "Selector",
+        "dkim_key_length": "DKIM-Schlüssellänge (bits)",
         "domain": "Domain",
         "domain_admins": "Domain-Administratoren",
         "domain_aliases": "Domain-Aliasse",
         "domain_quota": "Gesamtspeicher",
+        "domain_quota_total": "Domain-Speicherplatz gesamt",
         "domains": "Domains",
         "edit": "Bearbeiten",
         "empty": "Keine Einträge vorhanden",
@@ -758,12 +764,15 @@
         "filter_table": "Filtern",
         "filters": "Filter",
         "fname": "Name",
+        "force_pw_update": "Erzwinge Passwortänderung bei nächstem Login",
+        "gal": "Globales Adressbuch",
         "hourly": "Stündlich",
         "in_use": "Prozentualer Gebrauch",
         "inactive": "Inaktiv",
         "insert_preset": "Beispiel \"%s\" laden",
         "kind": "Art",
         "last_mail_login": "Letzter Mail-Login",
+        "last_modified": "Zuletzt geändert",
         "last_pw_change": "Letzte Passwortänderung",
         "last_run": "Letzte Ausführung",
         "last_run_reset": "Als nächstes ausführen",
@@ -773,6 +782,9 @@
         "mailbox_defquota": "Standard-Quota",
         "mailbox_quota": "Max. Größe einer Mailbox",
         "mailboxes": "Mailboxen",
+        "max_aliases": "Max. mögliche Aliasse",
+        "max_mailboxes": "Max. mögliche Mailboxen",
+        "max_quota": "Max. Größe per Mailbox",
         "mins_interval": "Intervall (min)",
         "msg_num": "Anzahl Nachrichten",
         "multiple_bookings": "Mehrfachbuchen",
@@ -796,6 +808,7 @@
         "recipient_map_old": "Original-Empfänger",
         "recipient_map_old_info": "Der originale Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.",
         "recipient_maps": "Empfängerumschreibungen",
+        "relay_all": "Alle Empfänger-Adressen relayen",
         "remove": "Entfernen",
         "resources": "Ressourcen",
         "running": "In Ausführung",

+ 24 - 0
data/web/lang/lang.en-gb.json

@@ -439,6 +439,9 @@
         "target_domain_invalid": "Target domain %s is invalid",
         "targetd_not_found": "Target domain %s not found",
         "targetd_relay_domain": "Target domain %s is a relay domain",
+        "template_exists": "Template %s already exists",
+        "template_id_invalid": "Template ID %s invalid",
+        "template_name_invalid": "Template name invalid",
         "temp_error": "Temporary error",
         "text_empty": "Text must not be empty",
         "tfa_token_invalid": "TFA token invalid",
@@ -547,6 +550,7 @@
         "client_id": "Client ID",
         "client_secret": "Client secret",
         "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
+        "created_on": "Created on",
         "delete1": "Delete from source when completed",
         "delete2": "Delete messages on destination that are not on source",
         "delete2duplicates": "Delete duplicates on destination",
@@ -573,6 +577,7 @@
         "hostname": "Hostname",
         "inactive": "Inactive",
         "kind": "Kind",
+        "last_modified": "Last modified",
         "lookup_mx": "Destination is a regular expression to match against MX name (<code>.*google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
         "mailbox": "Edit mailbox",
         "mailbox_quota_def": "Default mailbox quota",
@@ -713,6 +718,7 @@
         "add_mailbox": "Add mailbox",
         "add_recipient_map_entry": "Add recipient map",
         "add_resource": "Add resource",
+        "add_template": "Add Template",
         "add_tls_policy_map": "Add TLS policy map",
         "address_rewriting": "Address rewriting",
         "alias": "Alias",
@@ -744,15 +750,20 @@
         "booking_ltnull": "Unlimited, but show as busy when booked",
         "booking_lt0_short": "Soft limit",
         "catch_all": "Catch-All",
+        "created_on": "Created on",
         "daily": "Daily",
         "deactivate": "Deactivate",
         "description": "Description",
         "disable_login": "Disallow login (incoming mail is still accepted)",
         "disable_x": "Disable",
+        "dkim_domains_selector": "Selector",
+        "dkim_key_length": "DKIM key length (bits)",
         "domain": "Domain",
         "domain_admins": "Domain administrators",
         "domain_aliases": "Domain aliases",
+        "domain_templates": "Domain Templates",
         "domain_quota": "Quota",
+        "domain_quota_total": "Total domain quota",
         "domains": "Domains",
         "edit": "Edit",
         "empty": "No results",
@@ -761,6 +772,8 @@
         "filter_table": "Filter table",
         "filters": "Filters",
         "fname": "Full name",
+        "force_pw_update": "Force password update at next login",
+        "gal": "Global Address List",
         "goto_ham": "Learn as <b>ham</b>",
         "goto_spam": "Learn as <b>spam</b>",
         "hourly": "Hourly",
@@ -769,6 +782,7 @@
         "insert_preset": "Insert example preset \"%s\"",
         "kind": "Kind",
         "last_mail_login": "Last mail login",
+        "last_modified": "Last modified",
         "last_pw_change": "Last password change",
         "last_run": "Last run",
         "last_run_reset": "Schedule next",
@@ -776,8 +790,12 @@
         "mailbox_defaults": "Default settings",
         "mailbox_defaults_info": "Define default settings for new mailboxes.",
         "mailbox_defquota": "Default mailbox size",
+        "mailbox_templates": "Mailbox Templates",
         "mailbox_quota": "Max. size of a mailbox",
         "mailboxes": "Mailboxes",
+        "max_aliases": "Max. aliases",
+        "max_mailboxes": "Max. possible mailboxes",
+        "max_quota": "Max. quota per mailbox",
         "mins_interval": "Interval (min)",
         "msg_num": "Message #",
         "multiple_bookings": "Multiple bookings",
@@ -803,6 +821,8 @@
         "recipient_map_old": "Original recipient",
         "recipient_map_old_info": "A recipient maps original destination must be valid email addresses or a domain name.",
         "recipient_maps": "Recipient maps",
+        "relay_all": "Relay all recipients",
+        "relay_unknown": "Relay unknown mailboxes",
         "remove": "Remove",
         "resources": "Resources",
         "running": "Running",
@@ -839,6 +859,8 @@
         "table_size_show_n": "Show %s items",
         "target_address": "Goto address",
         "target_domain": "Target domain",
+        "templates": "Templates",
+        "template": "Template",
         "tls_enforce_in": "Enforce TLS incoming",
         "tls_enforce_out": "Enforce TLS outgoing",
         "tls_map_dest": "Destination",
@@ -1012,6 +1034,8 @@
         "settings_map_added": "Added settings map entry",
         "settings_map_removed": "Removed settings map ID %s",
         "sogo_profile_reset": "SOGo profile for user %s was reset",
+        "template_added": "Added template %s",
+        "template_modified": "Changes to template %s have been saved",
         "tls_policy_map_entry_deleted": "TLS policy map ID %s has been deleted",
         "tls_policy_map_entry_saved": "TLS policy map entry \"%s\" has been saved",
         "ui_texts": "Saved changes to UI texts",

+ 11 - 0
data/web/lang/lang.es-es.json

@@ -383,6 +383,7 @@
         "hostname": "Hostname",
         "inactive": "Inactivo",
         "kind": "Tipo",
+        "last_modified": "Última modificación",
         "mailbox": "Editar buzón",
         "mailbox_quota_def": "Cuota de buzón predeterminada",
         "max_aliases": "Máx. alias:",
@@ -488,10 +489,13 @@
         "deactivate": "Desactivar",
         "description": "Descripción",
         "disable_x": "Desactivar",
+        "dkim_domains_selector": "Selector",
+        "dkim_key_length": "Longitud de la llave DKIM (bits)",
         "domain": "Dominio",
         "domain_admins": "Administradores por dominio",
         "domain_aliases": "Alias de dominio",
         "domain_quota": "Cuota",
+        "domain_quota_total": "Cuota total del dominio",
         "domains": "Dominios",
         "edit": "Editar",
         "empty": "Sin resultados",
@@ -500,14 +504,20 @@
         "filter_table": "Filtrar tabla",
         "filters": "Filtros",
         "fname": "Nombre completo",
+        "force_pw_update": "Forzar cambio de contraseña en el próximo inicio de sesión",
+        "gal": "Lista global de direcciones (GAL)",
         "hourly": "Cada hora",
         "in_use": "En uso (%)",
         "inactive": "Inactivo",
         "kind": "Tipo",
+        "last_modified": "Última modificación",
         "last_run": "Última ejecución",
         "mailbox_defquota": "Tamaño de buzón predeterminado",
         "mailbox_quota": "Tamaño máx. de cuota",
         "mailboxes": "Buzones",
+        "max_aliases": "Máx. alias posibles",
+        "max_mailboxes": "Máx. buzones posibles",
+        "max_quota": "Máx. cuota por buzón",
         "mins_interval": "Intervalo (min)",
         "msg_num": "Mensaje #",
         "multiple_bookings": "Reservas multiples",
@@ -523,6 +533,7 @@
         "recipient_map_old": "Destinatario original",
         "recipient_map_old_info": "El destino original de una regla de destinatario debe ser una dirección de correo electrónico válida o un nombre de dominio.",
         "recipient_maps": "Reglas de destinatario",
+        "relay_all": "Retransmitir todos los destinatarios",
         "remove": "Eliminar",
         "resources": "Recursos",
         "running": "En marcha",

+ 12 - 1
data/web/lang/lang.fi-fi.json

@@ -433,6 +433,7 @@
         "hostname": "Hostname",
         "inactive": "Passiivinen",
         "kind": "Kiltti",
+        "last_modified": "Viimeksi muokattu",
         "mailbox": "Muokkaa sähköposti tiliä",
         "mailbox_quota_def": "Sähköpostin oletus kiintiö",
         "max_aliases": "Maks. Aliaksia",
@@ -558,11 +559,14 @@
         "daily": "Päivittäin",
         "deactivate": "Deaktivoi",
         "description": "Kuvaus",
-        "disable_x": "Poista käytöstä",
+        "disable_x": "Poista käytöstä",        
+        "dkim_domains_selector": "Valitsin",
+        "dkim_key_length": "DKIM avaimen pituus (bits)",
         "domain": "Verkkotunnukset",
         "domain_admins": "Verkkotunnuksien järjestelmänvalvojat",
         "domain_aliases": "Domain alueiden aliakset",
         "domain_quota": "Kiintiö",
+        "domain_quota_total": "Verkkotunnuksen kokonaiskiintiö",
         "domains": "Verkkotunnukset",
         "edit": "Muokkaa",
         "empty": "Ei tuloksia",
@@ -571,16 +575,22 @@
         "filter_table": "Suodata taulu",
         "filters": "Suodattimet",
         "fname": "Koko nimi",
+        "force_pw_update": "Pakota salasanan vaihto seuraavan sisään kirjautumisen jälkeen",
+        "gal": "Yleinen osoite luettelo",
         "hourly": "Tunnin välein",
         "in_use": "Käytössä (%)",
         "inactive": "Epäaktiivinen",
         "kind": "Sellainen",
+        "last_modified": "Viimeksi muokattu",
         "last_run": "Viimeisin suoritus",
         "last_run_reset": "Ajoita seuraava",
         "mailbox": "Postilaatikko",
         "mailbox_defquota": "Tilin koko",
         "mailbox_quota": "Kiintiön koko",
         "mailboxes": "Sähköposti tilit",
+        "max_aliases": "Max. mahdolliset aliakset",
+        "max_mailboxes": "Max. mahdolliset sähkö postilaatikot",
+        "max_quota": "Maks. Kiintiö sähköposti laatikkoa kohden",
         "mins_interval": "Aikaväli (min)",
         "msg_num": "Viestejä #",
         "multiple_bookings": "Useita varauksia",
@@ -600,6 +610,7 @@
         "recipient_map_old": "Alkuperäinen vastaanottaja",
         "recipient_map_old_info": "Vastaanottajan yhdistämis määritysten alkuperäisen kohteen on oltava kelvollinen sähköposti osoite tai verkkotunnus alueen nimi.",
         "recipient_maps": "Vastaanottajien yhdistämis määritykset",
+        "relay_all": "Välitä kaikki vastaanottajat",
         "remove": "Poista",
         "resources": "Resursseja",
         "running": "Running",

+ 12 - 1
data/web/lang/lang.fr-fr.json

@@ -512,6 +512,7 @@
         "hostname": "Nom d'hôte",
         "inactive": "Inactif",
         "kind": "Type",
+        "last_modified": "Dernière modification",
         "mailbox": "Edition de la boîte mail",
         "mailbox_quota_def": "Quota par défaut de la boîte",
         "max_aliases": "Nombre max. d'alias",
@@ -661,24 +662,30 @@
         "description": "Description",
         "disable_login": "Refuser l’ouverture de session (le courrier entrant est toujours accepté)",
         "disable_x": "Désactiver",
+        "dkim_domains_selector": "Sélecteur",
+        "dkim_key_length": "Longueur de la clé DKIM (bits)",
         "domain": "Domaine",
         "domain_admins": "Administrateurs de domaine",
         "domain_aliases": "Alias de domaine",
         "domain_quota": "Quota",
+        "domain_quota_total": "Quota total du domaine",
         "domains": "Domaines",
         "edit": "Editer",
         "empty": "Pas de résulats",
         "enable_x": "Activer",
         "excludes": "Exclut",
         "filter_table": "Table de filtre",
-        "filters": "Filtres",
+        "filters": "Filtres",   
         "fname": "Nom complet",
+        "force_pw_update": "Forcer la mise à jour du mot de passe à la prochaine ouverture de session",
+        "gal": "Carnet d'Adresses Global (GAL)",     
         "hourly": "Horaire",
         "in_use": "Utilisé (%)",
         "inactive": "Inactif",
         "insert_preset": "Insérer un exemple de préréglage \"%s\"",
         "kind": "Type",
         "last_mail_login": "Dernière connexion mail",
+        "last_modified": "Dernière modification",
         "last_run": "Dernière éxécution",
         "last_run_reset": "Calendrier suivant",
         "mailbox": "Mailbox",
@@ -687,6 +694,9 @@
         "mailboxes": "Boîtes mail",
         "mailbox_defaults": "Paramètres par défaut",
         "mailbox_defaults_info": "Définir les paramètres par défaut pour les nouvelles boîtes aux lettres.",
+        "max_aliases": "Nombre maximal d'alias",
+        "max_mailboxes": "Nombre maximal de boîtes",
+        "max_quota": "Quota max. par boîte mail",
         "mins_interval": "Intervalle (min)",
         "msg_num": "Message #",
         "multiple_bookings": "Réservations multiples",
@@ -710,6 +720,7 @@
         "recipient_map_old": "Destinataire original",
         "recipient_map_old_info": "Une carte de destination originale doit être une adresse e-mail valide ou un nom de domaine.",
         "recipient_maps": "Cartes des bénéficiaires",
+        "relay_all": "Relayer tous les destinataires",
         "remove": "Supprimer",
         "resources": "Ressources",
         "running": "En fonctionnement",

+ 1 - 0
data/web/lang/lang.hu-hu.json

@@ -118,6 +118,7 @@
         "filter_table": "Szűrő-táblázat",
         "filters": "Szűrők",
         "fname": "Teljes név",
+        "force_pw_update": "Új jelszót kell beállítania a csoportos szolgáltatások eléréséhez.",
         "hourly": "Óránként",
         "in_use": "Foglalt (%)",
         "inactive": "Inaktív",

+ 11 - 0
data/web/lang/lang.it-it.json

@@ -509,6 +509,7 @@
         "bcc_dest_format": "BCC destination must be a single valid email address.",
         "client_id": "Client ID",
         "client_secret": "Client secret",
+        "created_on": "Creato il",
         "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
         "delete1": "Elimina dalla sorgente al termine",
         "delete2": "Delete messages on destination that are not on source",
@@ -536,6 +537,7 @@
         "hostname": "Hostname",
         "inactive": "Inattivo",
         "kind": "Genere",
+        "last_mail_login": "Last mail login",
         "lookup_mx": "Destination is a regular expression to match against MX name (<code>.*google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
         "mailbox": "Modifica casella di posta",
         "mailbox_quota_def": "Default mailbox quota",
@@ -706,15 +708,19 @@
         "booking_custom_short": "Hard limit",
         "booking_ltnull": "Unlimited, but show as busy when booked",
         "booking_lt0_short": "Soft limit",
+        "created_on": "Creato il",
         "daily": "Giornaliero",
         "deactivate": "Disattiva",
         "description": "Descrizione",
         "disable_login": "Disabilita l'accesso (la posta in arrivo viene correttamente recapitata)",
         "disable_x": "Disabilita",
+        "dkim_domains_selector": "Selettore",
+        "dkim_key_length": "Lunghezza chiave DKIM (bits)",
         "domain": "Dominio",
         "domain_admins": "Amministratori di dominio",
         "domain_aliases": "Alias di domini",
         "domain_quota": "Spazio",
+        "domain_quota_total": "Spazio totale dominio",
         "domains": "Domini",
         "edit": "Modifica",
         "empty": "Nessun risultato",
@@ -729,6 +735,7 @@
         "insert_preset": "Insert example preset \"%s\"",
         "kind": "Tipo",
         "last_mail_login": "Last mail login",
+        "last_modified": "Ultima modifica",
         "last_pw_change": "Ultima modifica della password",
         "last_run": "Ultima esecuzione",
         "last_run_reset": "Schedule next",
@@ -738,6 +745,9 @@
         "mailbox_defquota": "Dimensione predefinita della casella di posta",
         "mailbox_quota": "Massima dimensione della casella",
         "mailboxes": "Caselle",
+        "max_aliases": "Numero massimo alias",
+        "max_mailboxes": "Numero massimo caselle di posta",
+        "max_quota": "Massimo spazio per casella",
         "mins_interval": "Intervallo (min)",
         "msg_num": "Messaggio #",
         "multiple_bookings": "Prenotazioni multiple",
@@ -761,6 +771,7 @@
         "recipient_map_old": "Original recipient",
         "recipient_map_old_info": "A recipient maps original destination must be valid email addresses or a domain name.",
         "recipient_maps": "Recipient maps",
+        "relay_all": "Trasmettere a tutti i destinatari",
         "remove": "Rimuovi",
         "resources": "Risorse",
         "running": "In esecuzione",

+ 11 - 0
data/web/lang/lang.ko-kr.json

@@ -487,6 +487,7 @@
         "hostname": "Hostname",
         "inactive": "Inactive",
         "kind": "Kind",
+        "last_modified": "Last modified",
         "mailbox": "Edit mailbox",
         "mailbox_quota_def": "Default mailbox quota",
         "max_aliases": "Max. aliases",
@@ -627,10 +628,13 @@
         "description": "설명",
         "disable_login": "로그인 비활성화 (오는 메일은 계속 받습니다.)",
         "disable_x": "비활성화",
+        "dkim_domains_selector": "선택기",
+        "dkim_key_length": "DKIM key 길이 (bits)",
         "domain": "도메인",
         "domain_admins": "도메인 관리자",
         "domain_aliases": "도메인 별칭",
         "domain_quota": "한도",
+        "domain_quota_total": "도메인에 할당할 디스크 크기",
         "domains": "도메인",
         "edit": "수정",
         "empty": "결과 없음",
@@ -639,18 +643,24 @@
         "filter_table": "Filter table",
         "filters": "Filters",
         "fname": "Full name",
+        "force_pw_update": "그룹웨어 관련 서비스에 접근하기 위해서는 새 비밀번호를 <b>꼭</b> 설정해야 합니다.",
+        "gal": "글로벌 주소 리스트",
         "hourly": "Hourly",
         "in_use": "In use (%)",
         "inactive": "Inactive",
         "insert_preset": "Insert example preset \"%s\"",
         "kind": "Kind",
         "last_mail_login": "Last mail login",
+        "last_modified": "Last modified",
         "last_run": "Last run",
         "last_run_reset": "Schedule next",
         "mailbox": "Mailbox",
         "mailbox_defquota": "Default mailbox size",
         "mailbox_quota": "Max. size of a mailbox",
         "mailboxes": "Mailboxes",
+        "max_aliases": "최대 별칭 주소",
+        "max_mailboxes": "최대 메일함 수",
+        "max_quota": "Max. quota per mailbox",
         "mins_interval": "Interval (min)",
         "msg_num": "Message #",
         "multiple_bookings": "Multiple bookings",
@@ -670,6 +680,7 @@
         "recipient_map_old": "Original recipient",
         "recipient_map_old_info": "A recipient maps original destination must be valid email addresses or a domain name.",
         "recipient_maps": "Recipient maps",
+        "relay_all": "모든 수신자에게 릴레이",
         "remove": "Remove",
         "resources": "Resources",
         "running": "Running",

+ 7 - 0
data/web/lang/lang.lv-lv.json

@@ -323,10 +323,12 @@
         "bcc_type": "BCC tips",
         "deactivate": "Deaktivizēt",
         "description": "Apraksts",
+        "dkim_key_length": "DKIM atslēgas garums (bits)",
         "domain": "Domēns",
         "domain_admins": "Domēna administratori",
         "domain_aliases": "Domēna aliases",
         "domain_quota": "Kvota",
+        "domain_quota_total": "Kopējā  domēna kvota",
         "domains": "Domēns",
         "edit": "Labot",
         "empty": "Nav rezultātu",
@@ -334,6 +336,7 @@
         "filter_table": "Filtra tabula",
         "filters": "Filtri",
         "fname": "Pilns vārds",
+        "force_pw_update": "Piespiedu paroles atjaunošana pie nākošās pieslēgšanās",
         "in_use": "Lietošanā (%)",
         "inactive": "Neaktīvs",
         "kind": "Veids",
@@ -341,6 +344,9 @@
         "last_run_reset": "Nākamais grafiks",
         "mailbox_quota": "Maks. pastkastes izmērs",
         "mailboxes": "Pastkaste",
+        "max_aliases": "Maks. iespejamās aliases",
+        "max_mailboxes": "Maks. iespējamās pastkastes",
+        "max_quota": "Maks. kvota uz pastkasti",
         "mins_interval": "Intervāls (min)",
         "msg_num": "Vēstule #",
         "multiple_bookings": "Vairāki rezervējumi",
@@ -352,6 +358,7 @@
         "recipient_map_new": "Jauns saņēmējs",
         "recipient_map_old": "Oriģinālais saņēmējs",
         "recipient_maps": "Saņēmēja kartes",
+        "relay_all": "Pārsūtīt visus saņēmējus",
         "remove": "Noņemt",
         "resources": "Resursi",
         "running": "Darbojas",

+ 11 - 0
data/web/lang/lang.nl-nl.json

@@ -504,6 +504,7 @@
         "hostname": "Hostname",
         "inactive": "Inactief",
         "kind": "Soort",
+        "last_modified": "Voor het laatst bijgewerkt op",
         "mailbox": "Wijzig mailbox",
         "mailbox_quota_def": "Standaard mailboxquota",
         "max_aliases": "Maximaal aantal aliassen",
@@ -651,10 +652,13 @@
         "description": "Beschrijving",
         "disable_login": "Weiger aanmelden (inkomende mail blijft binnenkomen)",
         "disable_x": "Schakel uit",
+        "dkim_domains_selector": "Selector",
+        "dkim_key_length": "Grootte van key (bits)",
         "domain": "Domein",
         "domain_admins": "Domeinadministrators",
         "domain_aliases": "Domeinaliassen",
         "domain_quota": "Quota",
+        "domain_quota_m": "Totale domeinquota",
         "domains": "Domeinen",
         "edit": "Wijzig",
         "empty": "Geen resultaten",
@@ -663,12 +667,15 @@
         "filter_table": "Filtertabel",
         "filters": "Filters",
         "fname": "Volledige naam",
+        "force_pw_update": "Vereis nieuw wachtwoord bij eerstvolgende login",
+        "gal": "Globale adreslijst",
         "hourly": "Ieder uur",
         "in_use": "In gebruik (%)",
         "inactive": "Inactief",
         "insert_preset": "Voeg voorbeelden in \"%s\"",
         "kind": "Soort",
         "last_mail_login": "Laatste mail login",
+        "last_modified": "Voor het laatst bijgewerkt op",
         "last_run": "Laatst uitgevoerd",
         "last_run_reset": "Plan volgende",
         "mailbox": "Mailbox",
@@ -677,6 +684,9 @@
         "mailboxes": "Mailboxen",
         "mailbox_defaults": "Standaardinstellingen",
         "mailbox_defaults_info": "Stel standaardinstellingen in voor nieuwe mailboxen.",
+        "max_aliases": "Maximaal aantal aliassen",
+        "max_mailboxes": "Maximaal aantal mailboxen",
+        "max_quota": "Mailboxquota",
         "mins_interval": "Interval (min)",
         "msg_num": "Bericht #",
         "multiple_bookings": "Meerdere boekingen",
@@ -699,6 +709,7 @@
         "recipient_map_old": "Oorspronkelijke ontvanger",
         "recipient_map_old_info": "De oorspronkelijke bestemming van een ontvanger-map dient een geldig mailadres of domeinnaam te zijn.",
         "recipient_maps": "Ontvanger-maps",
+        "relay_all": "Forward alle ontvangers",
         "remove": "Verwijder",
         "resources": "Resources",
         "running": "Wordt uitgevoerd",

+ 6 - 0
data/web/lang/lang.pl-pl.json

@@ -233,10 +233,12 @@
         "daily": "Co dzień",
         "deactivate": "Wyłącz",
         "description": "Opis",
+        "dkim_key_length": "Długość klucza DKIM (bity)",
         "domain": "Domena",
         "domain_admins": "Administratorzy domeny",
         "domain_aliases": "Aliasy domeny",
         "domain_quota": "Limit wielkości",
+        "domain_quota_total": "Łączny limit domeny",
         "domains": "Domeny",
         "edit": "Edytuj",
         "empty": "Brak wyników",
@@ -250,6 +252,9 @@
         "last_run": "Ostatnie uruchomienie",
         "mailbox_quota": "Maks. wielkość skrzynki",
         "mailboxes": "Skrzynki",
+        "max_aliases": "Maks. liczba aliasów",
+        "max_mailboxes": "Maks. liczba skrzynek",
+        "max_quota": "Maks. wielkość skrzynki",
         "mins_interval": "Zakres (min)",
         "msg_num": "Wiadomość #",
         "multiple_bookings": "Wielokrotne rejestracje",
@@ -258,6 +263,7 @@
         "no_record_single": "Brak rekordu",
         "quarantine_notification": "Powiadomienia o kwarantannie",
         "quick_actions": "Szybkie działania",
+        "relay_all": "Przekaż wszystkim odbiorcom",
         "remove": "Usuń",
         "resources": "Zasoby",
         "spam_aliases": "Alias tymczasowy",

+ 7 - 1
data/web/lang/lang.pt-pt.json

@@ -9,7 +9,7 @@
         "backup_mx_options": "Opções Backup MX:",
         "description": "Descrição:",
         "domain": "Domínio",
-        "domain_quota_m": "Total de espaço por domínio(MiB):",
+        "domain_quota_m": "Total de espaço por domínio (MiB):",
         "full_name": "Nome:",
         "mailbox_quota_m": "Máximo espaço por conta (MiB):",
         "mailbox_username": "Usuário (primeira parte do endereço de email):",
@@ -161,10 +161,12 @@
         "aliases": "Apelidos",
         "backup_mx": "Backup MX",
         "description": "Descrição:",
+        "dkim_key_length": "Tamanho do registro DKIM (bits)",
         "domain": "Domínio",
         "domain_admins": "Administradores de domínio",
         "domain_aliases": "Encaminhamento de Domínio",
         "domain_quota": "Espaço",
+        "domain_quota_total": "Total de espaço por domínio",
         "domains": "Domínios",
         "edit": "Alterar",
         "filter_table": "Procurar",
@@ -172,9 +174,13 @@
         "in_use": "Em uso (%)",
         "mailbox_quota": "Espaço máximo da Conta",
         "mailboxes": "Contas",
+        "max_aliases": "Máximo de apelidos",
+        "max_mailboxes": "Máximo de contas",
+        "max_quota": "Máximo espaço por conta",
         "msg_num": "Mensagens",
         "no_record": "Nenhum registro",
         "no_record_single": "Nenhum registro",
+        "relay_all": "Relay para todas as contas",
         "remove": "Remover",
         "target_address": "Encaminhar para",
         "target_domain": "Domínio Destino",

+ 13 - 0
data/web/lang/lang.ro-ro.json

@@ -511,6 +511,7 @@
         "client_id": "ID Client",
         "client_secret": "Secret client",
         "comment_info": "Un comentariu privat nu este vizibil pentru utilizator, în timp ce un comentariu public este afișat ca un tooltip când se trece peste el într-o privire de ansamblu asupra utilizatorilor",
+        "created_on": "Creat în",
         "delete1": "Șterge de la sursă când ai terminat",
         "delete2": "Șterge mesajele la destinație care nu sunt la sursă",
         "delete2duplicates": "Șterge duplicate la destinație",
@@ -537,6 +538,7 @@
         "hostname": "Nume gazdă",
         "inactive": "Inactiv",
         "kind": "Fel",
+        "last_modified": "Ultima modificare",
         "lookup_mx": "Destinația este o expresie regulată care potrivită cu numele MX (<code>.*google\\.com</code> pentru a direcționa toate e-mailurile vizate către un MX care se termină în google.com peste acest hop)",
         "mailbox": "Editează căsuța poștală",
         "mailbox_quota_def": "Cota implicită a căsuței poștale",
@@ -707,15 +709,19 @@
         "booking_ltnull": "Nelimitat, dar arată ca ocupat atunci când este rezervat",
         "booking_lt0_short": "Limită redusă",
         "catch_all": "Prinde-Tot",
+        "created_on": "Creat în",
         "daily": "Zilnic",
         "deactivate": "Deactivează",
         "description": "Descriere",
         "disable_login": "Nu permiteți autentificarea",
         "disable_x": "Dezactivează",
+        "dkim_domains_selector": "Selector",
+        "dkim_key_length": "Lungimea cheii DKIM (biți)",
         "domain": "Domeniu",
         "domain_admins": "Administratori domeniu",
         "domain_aliases": "Aliasuri de domenii",
         "domain_quota": "Cotă",
+        "domain_quota_total": "Cota totală domeniu",
         "domains": "Domenii",
         "edit": "Editează",
         "empty": "Nici un rezultat",
@@ -724,6 +730,8 @@
         "filter_table": "Tabel filtre",
         "filters": "Filtre",
         "fname": "Nume complet",
+        "force_pw_update": "Forțează o actualizare a parolei la următoarea conectare",
+        "gal": "Lista adreselor globale",
         "goto_ham": "Învață ca <b>ham</b>",
         "goto_spam": "Învață ca <b>spam</b>",
         "hourly": "Din oră în oră",
@@ -732,6 +740,7 @@
         "insert_preset": "Inserați un exemplu presetat \"%s\"",
         "kind": "Fel",
         "last_mail_login": "Ultima autentificare pe mail",
+        "last_modified": "Ultima modificare",
         "last_pw_change": "Ultima modificare a parolei",
         "last_run": "Ultima rulare",
         "last_run_reset": "Programează următorul",
@@ -741,6 +750,9 @@
         "mailboxes": "Cutii poștale",
         "mailbox_defaults": "Setări implicite",
         "mailbox_defaults_info": "Definiți setările implicite pentru cutiile poștale noi.",
+        "max_aliases": "Număr maxim posibil de aliasuri",
+        "max_mailboxes": "Număr maxim posibil de cutii poștale",
+        "max_quota": "Cotă maximă pentru cutia poștală",
         "mins_interval": "Interval (min)",
         "msg_num": "Mesaj #",
         "multiple_bookings": "Rezervări multiple",
@@ -766,6 +778,7 @@
         "recipient_map_old": "Destinatar original",
         "recipient_map_old_info": "Destinația originală a hărților destinatarilor trebuie să fie adrese de email valide sau nume de domeniu.",
         "recipient_maps": "Hărți destinatar",
+        "relay_all": "Retransmite toți destinatarii",
         "remove": "Elimină",
         "resources": "Resurse",
         "running": "Rulare",

+ 13 - 0
data/web/lang/lang.ru-ru.json

@@ -511,6 +511,7 @@
         "client_id": "ID клиента",
         "client_secret": "Секретный ключ пользователя",
         "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя",
+        "created_on": "Дата создания",
         "delete1": "Удаление из источника после завершения",
         "delete2": "Удаление писем по месту назначения, которые не находятся на исходном",
         "delete2duplicates": "Удаление дубликатов по назначению",
@@ -537,6 +538,7 @@
         "hostname": "Имя хоста",
         "inactive": "Неактивный",
         "kind": "Тип",
+        "last_modified": "Последние изменения",
         "lookup_mx": "Назначение на основе резовинга MX записи по регулярному выражению (<code>.*\\.example\\.com$</code> для маршрутизации всей почты через этот хост, если MX заканчивающийся на example.com)",
         "mailbox": "Изменение почтового аккаунта",
         "mailbox_quota_def": "Квота по умолчанию",
@@ -705,15 +707,19 @@
         "booking_ltnull": "Неограниченный, занят при бронировании",
         "booking_lt0_short": "Неограниченный лимит",
         "catch_all": "Catch-all",
+        "created_on": "Дата создания",
         "daily": "Раз в день",
         "deactivate": "Отключить",
         "description": "Описание",
         "disable_login": "Вход в систему запрещен",
         "disable_x": "Отключить",
+        "dkim_domains_selector": "Selector",
+        "dkim_key_length": "Длина DKIM ключа (bits)",
         "domain": "Домен",
         "domain_admins": "Администраторы домена",
         "domain_aliases": "Псевдонимы доменов",
         "domain_quota": "Квота",
+        "domain_quota_total": "Квота домена",
         "domains": "Домены",
         "edit": "Изменить",
         "empty": "Пусто",
@@ -722,6 +728,8 @@
         "filter_table": "Поиск",
         "filters": "Фильтры",
         "fname": "Полное имя",
+        "force_pw_update": "Требовать смены пароля при следующем входе в систему",
+        "gal": "GAL - Глобальная адресная книга",
         "goto_ham": "Запомнить как <b>полезную почту</b>",
         "goto_spam": "Запомнить как <b>спам</b>",
         "hourly": "Раз в час",
@@ -730,6 +738,7 @@
         "insert_preset": "Вставить пример \"%s\"",
         "kind": "Тип",
         "last_mail_login": "Последний вход",
+        "last_modified": "Последние изменения",
         "last_pw_change": "Последняя смена пароля",
         "last_run": "Последний запуск",
         "last_run_reset": "Следующей запуск",
@@ -739,6 +748,9 @@
         "mailbox_defquota": "Квота по умолчанию",
         "mailbox_quota": "Макс. квота почт. ящика",
         "mailboxes": "Почтовые ящики",
+        "max_aliases": "Максимум псевдонимов",
+        "max_mailboxes": "Максимум почтовых ящиков",
+        "max_quota": "Максимальная квота почтового аккаунта",
         "mins_interval": "Интервал (в минутах)",
         "msg_num": "Писем",
         "multiple_bookings": "Несколько бронирований",
@@ -764,6 +776,7 @@
         "recipient_map_old": "Получатель",
         "recipient_map_old_info": "Должен быть валидный почтовым ящиком или доменом.",
         "recipient_maps": "Перезапись получателя",
+        "relay_all": "Ретрансляция всех получателей",
         "remove": "Удалить",
         "resources": "Ресурсы",
         "running": "В процессе",

+ 13 - 0
data/web/lang/lang.sk-sk.json

@@ -510,6 +510,7 @@
         "client_id": "ID klienta",
         "client_secret": "Klientský tajný kľúč",
         "comment_info": " Súkromný komentár nie je viditeľný používateľovi, na rozdiel od verejného komentára, ktorý je prezentovaný ako popis v prehľade používateľov",
+        "created_on": "Vytvorené",
         "delete1": "Vymazať zo zdrojovej schránky, po dokončení prenosu",
         "delete2": "Vymazať správy v cieľovej schránke, ak nie sú v zdrojovej",
         "delete2duplicates": "Vymazať duplikáty v cieľovej schránke",
@@ -536,6 +537,7 @@
         "hostname": "Hostiteľ",
         "inactive": "Neaktívny",
         "kind": "Druh",
+        "last_modified": "Naposledy upravené",
         "lookup_mx": "Cieľ je regulárny výraz ktorý sa zhoduje s MX záznamom (<code>.*google\\.com</code> smeruje všetku poštu na MX ktoré sú cieľom pre google.com cez tento skok)",
         "mailbox": "Upraviť mailovú schránku",
         "mailbox_quota_def": "Predvolená veľkosť mailovej schránky",
@@ -707,15 +709,19 @@
         "booking_ltnull": "Bez limitu, ale zobraziť obsadené po rezervácii",
         "booking_lt0_short": "Voľný limit",
         "catch_all": "Doménový kôš",
+        "created_on": "Vytvorené",
         "daily": "Denný",
         "deactivate": "Deaktivovať",
         "description": "Popis",
         "disable_login": "Zablokovať prihlásenie (nevzťahuje sa na prichádzajúcu poštu)",
         "disable_x": "Pozastaviť",
+        "dkim_domains_selector": "Selektor",
+        "dkim_key_length": "Dĺžka DKIM kľúča (bity)",
         "domain": "Doména",
         "domain_admins": "Administrátori domény",
         "domain_aliases": "Alias domény",
         "domain_quota": "Kvóta",
+        "domain_quota_total": "Celkové kvóta domény",
         "domains": "Domény",
         "edit": "Upraviť",
         "empty": "Žiadne výsledky",
@@ -724,6 +730,8 @@
         "filter_table": "Filtrovať tabuľku",
         "filters": "Filtre",
         "fname": "Celé meno",
+        "force_pw_update": "Vynútiť zmenu hesla pri ďalšom prihlásení",
+        "gal": "Globálny zoznam adries",
         "goto_ham": "Považovať za <b>ham</b>",
         "goto_spam": "Považovať za <b>spam</b>",
         "hourly": "Hodinový",
@@ -732,6 +740,7 @@
         "insert_preset": "Vložiť vzor nastavenia \"%s\"",
         "kind": "Druh",
         "last_mail_login": "Posledné prihlásenie",
+        "last_modified": "Naposledy upravené",
         "last_pw_change": "Naposledy zmenené heslo",
         "last_run": "Posledné spustenie",
         "last_run_reset": "Znovu naplánovať",
@@ -741,6 +750,9 @@
         "mailbox_defquota": "Predvolená veľkosť schránky",
         "mailbox_quota": "Max. veľkosť schránky",
         "mailboxes": "Mailové schránky",
+        "max_aliases": "Max. počet aliasov",
+        "max_mailboxes": "Max. počet mailových schránok",
+        "max_quota": "Max. kvóta pre mailovú schránku",
         "mins_interval": "Interval (min)",
         "msg_num": "Počet správ",
         "multiple_bookings": "Viaceré rezervácie",
@@ -766,6 +778,7 @@
         "recipient_map_old": "Originálny príjemca",
         "recipient_map_old_info": "Originálny cieľ mapy príjemcu musí byť platná emailová adresa alebo meno domény.",
         "recipient_maps": "Mapy príjemcov",
+        "relay_all": "Preposielať všetkým príjemcom",
         "remove": "Odstrániť",
         "resources": "Zdroje",
         "running": "Bežiaci",

+ 13 - 0
data/web/lang/lang.sv-se.json

@@ -492,6 +492,7 @@
         "client_id": "Klient-ID",
         "client_secret": "Klienthemlighet",
         "comment_info": "En privat kommentar är inte synlig för användaren, medan en offentlig kommentar visas i användaröversikten",
+        "created_on": "Skapad vid",
         "delete1": "Ta bort meddelande från källservern när överföringen är slutförd.",
         "delete2": "Ta bort meddelanden från destinationsservern som inte finns på källservern",
         "delete2duplicates": "Ta bort dubbletter på destinationsservern",
@@ -518,6 +519,7 @@
         "hostname": "Värdnamn",
         "inactive": "Inaktiv",
         "kind": "Typ",
+        "last_modified": "Senast ändrad",
         "mailbox": "Ändra postlåda",
         "mailbox_quota_def": "Standard kvot på postlådor",
         "max_aliases": "Max antal alias",
@@ -663,15 +665,19 @@
         "booking_custom_short": "Hård gräns",
         "booking_ltnull": "Obegränsad antal, men visa resursen som upptagen när den är bokad",
         "booking_lt0_short": "Mjuk gräns",
+        "created_on": "Skapad vid",
         "daily": "Dagligen",
         "deactivate": "Inaktivera",
         "description": "Beskrivning",
         "disable_login": "Inaktivera inloggning (inkommande post kommer fortfarande tas emot)",
         "disable_x": "Inaktivera",
+        "dkim_domains_selector": "Välj",
+        "dkim_key_length": "DKIM-nyckellängd (bitar)",
         "domain": "Domän",
         "domain_admins": "Domänadministratörer",
         "domain_aliases": "Domänalias",
         "domain_quota": "Kvot",
+        "domain_quota_total": "Total kvot per postlåda",
         "domains": "Domäner",
         "edit": "Ändra",
         "empty": "Ingen information",
@@ -680,12 +686,15 @@
         "filter_table": "Filtrera tabellen",
         "filters": "Postfilter",
         "fname": "Fullständigt namn",
+        "force_pw_update": "Kräv uppdatering av lösenordet vid nästa inloggning",
+        "gal": "Global adressbok",
         "hourly": "Varje timme",
         "in_use": "Användning (%)",
         "inactive": "Inaktiv",
         "insert_preset": "Infoga exempelkoden \"%s\"",
         "kind": "Sort",
         "last_mail_login": "Senaste inloggningen",
+        "last_modified": "Senast ändrad",
         "last_run": "Senaste körningen",
         "last_run_reset": "Schemalägg nästa",
         "mailbox": "Postlåda",
@@ -694,6 +703,9 @@
         "mailboxes": "Postlådor",
         "mailbox_defaults": "Standardinställningar",
         "mailbox_defaults_info": "Standardinställningar för nya postlådor.",
+        "max_aliases": "Max antal alias",
+        "max_mailboxes": "Max antal postlådor",
+        "max_quota": "Max. kvot per postlåda",
         "mins_interval": "Interval (min)",
         "msg_num": "Antal meddelanden",
         "multiple_bookings": "Flera bokningar",
@@ -717,6 +729,7 @@
         "recipient_map_old": "Ursprunglig mottagaren",
         "recipient_map_old_info": "Den ursprungliga mottagaren måste vara en giltiga e-postadresser eller ett domännamn.",
         "recipient_maps": "Skriv om mottagaradressen",
+        "relay_all": "Vidarebefordra alla mottagare",
         "remove": "Ta bort",
         "resources": "Resurser",
         "running": "Körs",

+ 13 - 0
data/web/lang/lang.uk-ua.json

@@ -509,6 +509,7 @@
         "backup_mx_options": "Параметри резервного MX",
         "client_id": "ID клієнта",
         "client_secret": "Секретний ключ користувача",
+        "created_on": "Дата створення",
         "delete1": "Видалити з джерела після завершення",
         "delete2": "Видалити листи за місцем призначення, яких не має в джерелі",
         "delete2duplicates": "Видалити дублікати за місцем призначення",
@@ -531,6 +532,7 @@
         "hostname": "Ім'я хоста",
         "inactive": "Неактивний",
         "kind": "Тип",
+        "last_modified": "Останні зміни",
         "mailbox": "Редагувати поштову скриньку",
         "max_aliases": "Максимум псевдонімів",
         "max_mailboxes": "Максимум поштових скриньок",
@@ -704,13 +706,17 @@
         "booking_ltnull": "Необмежений, зайнятий під час бронювання",
         "booking_lt0_short": "М’який ліміт",
         "catch_all": "Catch-all",
+        "created_on": "Дата створення",
         "daily": "Раз на день",
         "description": "Опис",
         "disable_x": "Вимкнути",
+        "dkim_domains_selector": "Селектор",
+        "dkim_key_length": "Довжина ключа DKIM (біт)",
         "domain": "Домен",
         "domain_admins": "Адміністратори домену",
         "domain_aliases": "Псевдоніми доменів",
         "domain_quota": "Квота",
+        "domain_quota_total": "Загальна квота домену",
         "domains": "Домени",
         "edit": "Змінити",
         "empty": "Пусто",
@@ -719,18 +725,24 @@
         "filter_table": "Пошук",
         "filters": "Фильтри",
         "fname": "Повне ім'я",
+        "gal": "GAL - Глобальна адресна книга",
+        "force_pw_update": "Вимагати зміну пароля при наступному вході до системи",
         "goto_spam": "Запам'ятати як <b>спам</b>",
         "hourly": "Щогодини",
         "in_use": "Використано (%)",
         "inactive": "Неактивний",
         "insert_preset": "Вставити приклад \"%s\"",
         "kind": "Тип",
+        "last_modified": "Останні зміни",
         "last_pw_change": "Остання зміна пароля",
         "last_run": "Останній запуск",
         "last_run_reset": "Наступний запуск",
         "mailbox": "Поштовий акаунт",
         "mailbox_defaults_info": "Визначте параметри за замовчуванням для нових поштових акаунтів.",
         "mailbox_quota": "Макс. квота пошт. ящика",
+        "max_aliases": "Максимум псевдонімів",
+        "max_mailboxes": "Максимум поштових скриньок",
+        "max_quota": "Максимальна квота поштового акаунту",
         "mins_interval": "Інтервал (у хвилинах)",
         "msg_num": "Листів",
         "multiple_bookings": "Декілька бронювань",
@@ -751,6 +763,7 @@
         "recipient_map_new_info": "Повинен бути чинною поштовою скринькою.",
         "recipient_map_old": "Одержувач",
         "recipient_maps": "Перезапис одержувача",
+        "relay_all": "Ретрансляція всіх отримувачів",
         "remove": "Видалити",
         "resources": "Ресурси",
         "running": "В процесі",

+ 11 - 0
data/web/lang/lang.zh-cn.json

@@ -497,6 +497,7 @@
         "hostname": "主机名",
         "inactive": "禁用",
         "kind": "类型",
+        "last_modified": "最后修改",
         "mailbox": "编辑邮箱",
         "mailbox_quota_def": "默认邮箱配额",
         "max_aliases": "最大允许地址别名数",
@@ -640,10 +641,13 @@
         "description": "描述",
         "disable_login": "不允许登录 (仍然会接收邮件)",
         "disable_x": "关闭",
+        "dkim_domains_selector": "选择器",
+        "dkim_key_length": "DKIM密钥长度 (bits)",
         "domain": "域名",
         "domain_admins": "域名管理员",
         "domain_aliases": "域名别名",
         "domain_quota": "配额",
+        "domain_quota_total": "域名总配额",
         "domains": "域名",
         "edit": "编辑",
         "empty": "结果为空",
@@ -652,12 +656,15 @@
         "filter_table": "筛选表格",
         "filters": "过滤器",
         "fname": "全名",
+        "force_pw_update": "你<b>必须</b>设置一个新密码以继续使用群件相关服务。",
+        "gal": "全球地址簿",
         "hourly": "每小时",
         "in_use": "使用数 (%)",
         "inactive": "禁用",
         "insert_preset": "插入示例预设 \"%s\"",
         "kind": "类型",
         "last_mail_login": "最后的邮箱登录",
+        "last_modified": "最后修改",
         "last_run": "最后运行",
         "last_run_reset": "下一次运行",
         "mailbox": "邮箱",
@@ -666,6 +673,9 @@
         "mailboxes": "邮箱",
         "mailbox_defaults": "默认设置",
         "mailbox_defaults_info": "配置新邮箱的默认设置",
+        "max_aliases": "最大允许地址别名数",
+        "max_mailboxes": "最大允许邮箱数",
+        "max_quota": "每个邮箱的最大配额",
         "mins_interval": "间隔 (分钟)",
         "msg_num": "消息 #",
         "multiple_bookings": "登记限制",
@@ -685,6 +695,7 @@
         "recipient_map_old": "原收件人",
         "recipient_map_old_info": "原收件人必须为合法的邮件地址",
         "recipient_maps": "收件人映射",
+        "relay_all": "中继所有收件人",
         "remove": "删除",
         "resources": "日历资源",
         "running": "运行中",

+ 13 - 0
data/web/lang/lang.zh-tw.json

@@ -521,6 +521,7 @@
         "client_id": "用戶端 ID",
         "client_secret": "用戶端金鑰",
         "comment_info": "隱密備註不會被使用者看到,公開備註則會在使用者游標懸停於概述頁時顯示於提示框",
+        "created_on": "建立於",
         "delete1": "完成後將來源郵件刪除",
         "delete2": "刪除目的地信箱中存在但來源信箱中不存在的郵件",
         "delete2duplicates": "刪除目的地信箱中的重複郵件",
@@ -547,6 +548,7 @@
         "hostname": "主機名稱",
         "inactive": "停用",
         "kind": "種類",
+        "last_modified": "上次修改時間",
         "lookup_mx": "目的地是可以用來匹配 MX 紀錄的正規表達式 (<code>.*google\\.com</code> 會將所有 MX 結尾於 google.com 的郵件轉發到此主機。)",
         "mailbox": "編輯信箱",
         "mailbox_quota_def": "預設信箱容量配額",
@@ -717,15 +719,19 @@
         "booking_ltnull": "不限制登記數,但會在被登記後顯示為繁忙",
         "booking_lt0_short": "寬鬆限制",
         "catch_all": "全部接收",
+        "created_on": "建立於",
         "daily": "每天",
         "deactivate": "停用",
         "description": "描述",
         "disable_login": "不允許登入 (仍然會接收郵件)",
         "disable_x": "關閉",
+        "dkim_domains_selector": "選擇器",
+        "dkim_key_length": "DKIM 金鑰長度 (bits)",
         "domain": "域名",
         "domain_admins": "域名管理員",
         "domain_aliases": "域名別名",
         "domain_quota": "容量配額",
+        "domain_quota_total": "域名總容量配額",
         "domains": "域名",
         "edit": "編輯",
         "empty": "沒有結果",
@@ -734,6 +740,8 @@
         "filter_table": "篩選表格",
         "filters": "過濾器",
         "fname": "全名",
+        "force_pw_update": "你<b>必須</b>設定一個新密碼以繼續使用相關服務。",
+        "gal": "全域地址清單",
         "goto_ham": "學習為<b>非垃圾郵件</b>",
         "goto_spam": "學習為<b>垃圾郵件</b>",
         "hourly": "每小時",
@@ -742,6 +750,7 @@
         "insert_preset": "插入範例預設 \"%s\"",
         "kind": "種類",
         "last_mail_login": "上一次信箱登入",
+        "last_modified": "上次修改時間",
         "last_pw_change": "上一次密碼更改",
         "last_run": "上一次執行",
         "last_run_reset": "下一次執行",
@@ -751,6 +760,9 @@
         "mailbox_defquota": "預設信箱大小",
         "mailbox_quota": "最大信箱大小",
         "mailboxes": "信箱",
+        "max_aliases": "地址別名上限",
+        "max_mailboxes": "信箱數量上限",
+        "max_quota": "每個信箱的最大容量配額",
         "mins_interval": "間隔 (分鐘)",
         "msg_num": "訊息 #",
         "multiple_bookings": "重複登記",
@@ -776,6 +788,7 @@
         "recipient_map_old": "原收件人",
         "recipient_map_old_info": "原收件人必須為有效的郵件地址",
         "recipient_maps": "收件人規則表",
+        "relay_all": "中繼所有收件人",
         "remove": "刪除",
         "resources": "資源",
         "running": "執行中",

+ 1 - 0
data/web/mailbox.php

@@ -40,6 +40,7 @@ $template_data = [
   'domains' => $domains,
   'mailboxes' => $mailboxes,
   'lang_mailbox' => json_encode($lang['mailbox']),
+  'lang_rl' => json_encode($lang['ratelimit']),
   'lang_datatables' => json_encode($lang['datatables']),
 ];
 

+ 4 - 4
data/web/templates/base.twig

@@ -143,10 +143,10 @@
   var mailcow_info = {
     version_tag: '{{ mailcow_info.version_tag }}',
     last_version_tag: '{{ mailcow_info.last_version_tag }}',
-    updatedAt: '{{ mailcow_info.updatedAt }}',
-    project_url: '{{ mailcow_info.project_url }}',
-    project_owner: '{{ mailcow_info.project_owner }}',
-    project_repo: '{{ mailcow_info.project_repo }}',
+    updatedAt: '{{ mailcow_info.updated_at }}',
+    project_url: '{{ mailcow_info.git_project_url }}',
+    project_owner: '{{ mailcow_info.git_owner }}',
+    project_repo: '{{ mailcow_info.git_repo }}',
     branch: '{{ mailcow_info.mailcow_branch }}'
   };
 

+ 6 - 12
data/web/templates/debug.twig

@@ -58,19 +58,13 @@
                       </tr>
                       <tr>
                         <td>Version</td>
-                        <td class="text-break"><div>
-                          <p><b>{{ mailcow_info.version_tag }}</b></p>
-                          <p id="mailcow_update"></p> 
-                        </div></td>
-                      </tr>
-                      {% if mailcow_info.mailcow_branch|lower == "master" %}
-                      <tr>
-                        <td>Changelog</td>
-                        <td class="text-break"><a href="{{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">
-                          {{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}
-                        </a></td>
+                        <td class="text-break">
+                          <div class="fw-bolder">
+                            <p ><a href="#" id="maiclow_version">{{ mailcow_info.version_tag }}</a></p>
+                            <p id="mailcow_update"></p> 
+                          </div>
+                        </td>
                       </tr>
-                      {% endif %}
                       <tr>
                         <td>{{ lang.debug.current_time }}</td>
                         <td id="host_date" class="text-break">-</td>

+ 136 - 0
data/web/templates/edit/domain-templates.twig

@@ -0,0 +1,136 @@
+{% extends 'edit.twig' %}
+
+{% block inner_content %}
+{% if result %}
+<div id="dedit" class="tab-pane fade show active" role="tabpanel" aria-labelledby="domain-edit">
+  <form data-id="editdomain_template" class="form-horizontal" role="form" method="post">
+
+    <input type="hidden" value="0" name="active">
+    <input type="hidden" value="0" name="backupmx">
+    <input type="hidden" value="0" name="gal">
+    <input type="hidden" value="0" name="relay_all_recipients">
+    <input type="hidden" value="0" name="relay_unknown_only">
+
+    {% if mailcow_cc_role == 'admin' %}
+    <div class="row mb-4">
+      <label class="control-label col-sm-2" for="max_num_aliases_for_domain">{{ lang.mailbox.template }}</label>
+      <div class="col-sm-10">
+
+        <div class="input-group mb-3">
+          <input type="text" name="template" class="form-control" aria-label="Text input with dropdown button" value="{{ template.template }}" />
+        </div>
+      </div>
+
+    </div>
+    <div class="row mb-4">
+      <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
+      <div class="col-sm-10">
+        <div class="form-control tag-box">
+          <input id="tags" type="text" class="tag-input">
+          <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+          <input type="hidden" value='{{ template.attributes.tags|json_encode }}' name="tags" class="tag-values" />
+        </div>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="max_num_aliases_for_domain">{{ lang.edit.max_aliases }}</label>
+      <div class="col-sm-10">
+        <input type="number" class="form-control" name="max_num_aliases_for_domain" value="{{ template.attributes.max_num_aliases_for_domain }}">
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="max_num_mboxes_for_domain">{{ lang.edit.max_mailboxes }}</label>
+      <div class="col-sm-10">
+        <input type="number" class="form-control" name="max_num_mboxes_for_domain" value="{{ template.attributes.max_num_mboxes_for_domain }}">
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="def_quota_for_mbox">{{ lang.edit.mailbox_quota_def }}</label>
+      <div class="col-sm-10">
+        <input type="number" class="form-control" name="def_quota_for_mbox" value="{{ (template.attributes.def_quota_for_mbox / 1048576) }}">
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="max_quota_for_mbox">{{ lang.edit.max_quota }}</label>
+      <div class="col-sm-10">
+        <input type="number" class="form-control" name="max_quota_for_mbox" value="{{ (template.attributes.max_quota_for_mbox / 1048576) }}">
+      </div>
+    </div>
+    <div class="row mb-4">
+      <label class="control-label col-sm-2" for="max_quota_for_domain">{{ lang.edit.domain_quota }}</label>
+      <div class="col-sm-10">
+        <input type="number" class="form-control" name="max_quota_for_domain" value="{{ (template.attributes.max_quota_for_domain / 1048576) }}">
+      </div>
+    </div>
+    <div class="row">
+      <div class="offset-sm-2 col-sm-10">
+        <div class="checkbox">
+          <label><input type="checkbox" value="1" name="gal"{% if template.attributes.gal == '1' %} checked{% endif %}> {{ lang.edit.gal }}</label>
+          <small class="text-muted">{{ lang.edit.gal_info|raw }}</small>
+        </div>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <div class="offset-sm-2 col-sm-10">
+        <div class="checkbox">
+          <label><input type="checkbox" value="1" name="active"{% if template.attributes.active == '1' %} checked{% endif %}{% if mailcow_cc_role != 'admin' %} disabled{% endif %}> {{ lang.edit.active }}</label>
+        </div>
+      </div>
+    </div>
+    <hr>
+    <div class="row">
+      <label class="control-label col-sm-2">{{ lang.edit.ratelimit }}</label>
+      <div class="col-sm-10">
+        <input name="rl_value" type="number" value="{{ template.attributes.rl_value }}" autocomplete="off" class="form-control mb-4" placeholder="{{ lang.ratelimit.disabled }}">
+        <select name="rl_frame" class="form-control">
+          <option value="s"{% if template.attributes.rl_frame == 's' %} selected{% endif %}>{{ lang.ratelimit.second }}</option>
+          <option value="m"{% if template.attributes.rl_frame == 'm' %} selected{% endif %}>{{ lang.ratelimit.minute }}</option>
+          <option value="h"{% if template.attributes.rl_frame == 'h' %} selected{% endif %}>{{ lang.ratelimit.hour }}</option>
+          <option value="d"{% if template.attributes.rl_frame == 'd' %} selected{% endif %}>{{ lang.ratelimit.day }}</option>
+        </select>
+      </div>
+    </div>
+    {% endif %}
+    <hr>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="dkim_selector">{{ lang.admin.dkim_domains_selector }}</label>
+      <div class="col-sm-10">
+        <input class="form-control" id="dkim_selector" name="dkim_selector" value="{{ template.attributes.dkim_selector }}">
+      </div>
+    </div>
+    <div class="row mb-4">
+      <label class="control-label col-sm-2" for="key_size">{{ lang.admin.dkim_key_length }}</label>
+      <div class="col-sm-10">
+        <select data-style="btn btn-secondary btn-sm" class="form-control" id="key_size" name="key_size">
+          <option value="1024" data-subtext="bits" {% if template.attributes.key_size == 1024 %} selected{% endif %}>1024</option>
+          <option value="2048" data-subtext="bits" {% if template.attributes.key_size == 2048 %} selected{% endif %}>2048</option>
+        </select>
+      </div>
+    </div>
+    <hr>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2">{{ lang.edit.backup_mx_options }}</label>
+      <div class="col-sm-10">
+        <div class="checkbox">
+          <label><input type="checkbox" value="1" name="backupmx"{% if template.attributes.backupmx == '1' %} checked{% endif %}> {{ lang.edit.relay_domain }}</label>
+          <br>
+          <label><input type="checkbox" value="1" name="relay_all_recipients"{% if template.attributes.relay_all_recipients == '1' %} checked{% endif %}> {{ lang.edit.relay_all }}</label>
+          <p>{{ lang.edit.relay_all_info|raw }}</p>
+          <label><input type="checkbox" value="1" name="relay_unknown_only"{% if template.attributes.relay_unknown_only == '1' %} checked{% endif %}> {{ lang.edit.relay_unknown_only }}</label>
+          <br>
+          <p>{{ lang.edit.relay_transport_info|raw }}</p>
+        </div>
+      </div>
+    </div>
+    <hr>
+    <div class="row">
+      <div class="offset-sm-2 col-sm-10">
+        <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="editdomain_template" data-item="{{ template.id }}" data-api-url='edit/domain/template' data-api-attr='{}' href="#">{{ lang.admin.save }}</button>
+      </div>
+    </div>
+  </form>
+</div>
+{% else %}
+  {{ parent() }}
+{% endif %}
+{% endblock %}

+ 7 - 1
data/web/templates/edit/domain.twig

@@ -120,11 +120,17 @@
           </div>
         </div>
       </div>
-      <div class="row">
+      <div class="row mb-2">
         <div class="offset-sm-2 col-sm-10">
           <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="editdomain" data-item="{{ domain }}" data-api-url='edit/domain' data-api-attr='{}' href="#">{{ lang.admin.save }}</button>
         </div>
       </div>
+      <div class="row">
+        <div class="offset-sm-2 col-sm-10">
+          <small class="fst-italic d-block">{{ lang.edit.created_on }}: {{ result.created }}</small>
+          <small class="fst-italic d-block">{{ lang.edit.last_modified }}: {{ result.modified }}</small>
+        </div>
+      </div>
     </form>
     {% if dkim %}
     <hr>

+ 169 - 0
data/web/templates/edit/mailbox-templates.twig

@@ -0,0 +1,169 @@
+{% extends 'edit.twig' %}
+
+{% block inner_content %}
+{% if result %}
+<hr>
+<div id="medit" class="tab-pane fade show active" role="tabpanel" aria-labelledby="mailbox-edit">
+  <form class="form-horizontal" data-id="editmailbox_template" role="form" method="post">
+
+    <input type="hidden" value="default" name="sender_acl">
+    <input type="hidden" value="0" name="force_pw_update">
+    <input type="hidden" value="0" name="sogo_access">
+    <input type="hidden" value="0" name="protocol_access">     
+         
+    <div class="row mb-4">
+      <label class="control-label col-sm-2" for="template">{{ lang.mailbox.template }}</label>
+      <div class="col-sm-10">
+        <div class="input-group mb-3">
+          <input type="text" name="template" class="form-control" aria-label="Text input with dropdown button" value="{{ template.template }}" />
+        </div>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
+      <div class="col-sm-10">
+        <div class="form-control tag-box">
+          <input id="tags" type="text" class="tag-input">
+          <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+          <input type="hidden" value='{{ template.attributes.tags|json_encode }}' name="tags" class="tag-values" />
+        </div>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="quota">{{ lang.edit.quota_mb }}</label>
+      <div class="col-sm-10">
+        <input type="number" name="quota" class="w-100 form-control" min="0" value="{{ template.attributes.quota / 1048576 }}">
+        <small class="text-muted">0 = ∞</small>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2">{{ lang.user.quarantine_notification }}</label>
+      <div class="col-sm-10">
+        <div class="btn-group">
+          <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_never" autocomplete="off" value="never" {% if template.attributes.quarantine_notification == 'never' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_never">{{ lang.user.never }}</label>
+
+          <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_hourly" autocomplete="off" value="hourly" {% if template.attributes.quarantine_notification == 'hourly' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_hourly">{{ lang.user.hourly }}</label>
+
+          <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_daily" autocomplete="off" value="daily" {% if template.attributes.quarantine_notification == 'daily' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_daily">{{ lang.user.daily }}</label>
+
+          <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_weekly" autocomplete="off" value="weekly" {% if template.attributes.quarantine_notification == 'weekly' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_weekly">{{ lang.user.weekly }}</label>
+        </div>
+        <p class="text-muted"><small>{{ lang.user.quarantine_notification_info }}</small></p>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2">{{ lang.user.quarantine_category }}</label>
+      <div class="col-sm-10">
+        <div class="btn-group">
+          <input type="radio" class="btn-check" name="quarantine_category" id="quarantine_category_reject" autocomplete="off" value="reject" {% if template.attributes.quarantine_category == 'reject' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_category_reject">{{ lang.user.q_reject }}</label>
+          
+          <input type="radio" class="btn-check" name="quarantine_category" id="quarantine_category_add_header" autocomplete="off" value="add_header" {% if template.attributes.quarantine_category == 'add_header' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_category_add_header">{{ lang.user.q_add_header }}</label>
+          
+          <input type="radio" class="btn-check" name="quarantine_category" id="quarantine_category_all" autocomplete="off" value="all" {% if template.attributes.quarantine_category == 'all' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_category_all">{{ lang.user.q_all }}</label>
+        </div>
+        <p class="text-muted"><small>{{ lang.user.quarantine_category_info }}</small></p>
+      </div>
+    </div>
+    <div class="row mb-4">
+      <label class="control-label col-sm-2" for="sender_acl">{{ lang.user.tls_policy }}</label>
+      <div class="col-sm-10">
+        <div class="btn-group">
+          <input type="checkbox" class="btn-check" name="tls_enforce_in" id="tls_enforce_in" autocomplete="off" value="1" {% if template.attributes.tls_enforce_in == '1' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="tls_enforce_in">{{ lang.user.tls_enforce_in }}</label>
+          
+          <input type="checkbox" class="btn-check" name="tls_enforce_out" id="tls_enforce_out" autocomplete="off" value="1" {% if template.attributes.tls_enforce_out == '1' %}checked{% endif %}>
+          <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="tls_enforce_out">{{ lang.user.tls_enforce_out }}</label>
+        </div>
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="protocol_access">{{ lang.edit.allowed_protocols }}</label>
+      <div class="col-sm-10">
+        <select name="protocol_access" multiple class="form-control">
+          <option value="imap"{% if template.attributes.imap_access == '1' %} selected{% endif %}>IMAP</option>
+          <option value="pop3"{% if template.attributes.pop3_access == '1' %} selected{% endif %}>POP3</option>
+          <option value="smtp"{% if template.attributes.smtp_access == '1' %} selected{% endif %}>SMTP</option>
+          <option value="sieve"{% if template.attributes.sieve_access == '1' %} selected{% endif %}>Sieve</option>
+        </select>
+      </div>
+    </div>
+    <div class="row mb-4">
+      <label class="control-label col-sm-2">ACL</label>
+      <div class="col-sm-10">
+        <select id="template_user_acl" name="acl" size="10" multiple class="form-control">                  
+          <option value="spam_alias" {% if template.attributes.acl_spam_alias == '1' %} selected{% endif %}>{{ lang.acl["spam_alias"] }}</option>
+          <option value="tls_policy" {% if template.attributes.acl_tls_policy == '1' %} selected{% endif %}>{{ lang.acl["tls_policy"] }}</option>
+          <option value="spam_score" {% if template.attributes.acl_spam_score == '1' %} selected{% endif %}>{{ lang.acl["spam_score"] }}</option>
+          <option value="spam_policy" {% if template.attributes.acl_spam_policy == '1' %} selected{% endif %}>{{ lang.acl["spam_policy"] }}</option>
+          <option value="delimiter_action" {% if template.attributes.acl_delimiter_action == '1' %} selected{% endif %}>{{ lang.acl["delimiter_action"] }}</option>
+          <option value="syncjobs" {% if template.attributes.acl_syncjobs == '1' %} selected{% endif %}>{{ lang.acl["syncjobs"] }}</option>
+          <option value="eas_reset" {% if template.attributes.acl_eas_reset == '1' %} selected{% endif %}>{{ lang.acl["eas_reset"] }}</option>
+          <option value="sogo_profile_reset" {% if template.attributes.acl_sogo_profile_reset == '1' %} selected{% endif %}>{{ lang.acl["sogo_profile_reset"] }}</option>
+          <option value="pushover" {% if template.attributes.acl_pushover == '1' %} selected{% endif %}>{{ lang.acl["pushover"] }}</option>
+          <option value="quarantine" {% if template.attributes.acl_quarantine == '1' %} selected{% endif %}>{{ lang.acl["quarantine"] }}</option>
+          <option value="quarantine_attachments" {% if template.attributes.acl_quarantine_attachments == '1' %} selected{% endif %}>{{ lang.acl["quarantine_attachments"] }}</option>
+          <option value="quarantine_notification" {% if template.attributes.acl_quarantine_notification == '1' %} selected{% endif %}>{{ lang.acl["quarantine_notification"] }}</option>
+          <option value="quarantine_category" {% if template.attributes.acl_quarantine_category == '1' %} selected{% endif %}>{{ lang.acl["quarantine_category"] }}</option>
+          <option value="app_passwds" {% if template.attributes.acl_app_passwds == '1' %} selected{% endif %}>{{ lang.acl["app_passwds"] }}</option>
+        </select>
+      </div>
+    </div>
+    <div class="row mb-4">
+      <label class="control-label col-sm-2">{{ lang.acl.ratelimit }}</label>
+      <div class="col-sm-10">
+        <input name="rl_value" type="number" autocomplete="off" value="{{ template.attributes.rl_value }}" class="form-control mb-2" placeholder="{{ lang.ratelimit.disabled }}">
+        <select name="rl_frame" class="form-control">
+          <option value="s"{% if template.attributes.rl_frame == 's' %} selected{% endif %}>{{ lang.ratelimit.second }}</option>
+          <option value="m"{% if template.attributes.rl_frame == 'm' %} selected{% endif %}>{{ lang.ratelimit.minute }}</option>
+          <option value="h"{% if template.attributes.rl_frame == 'h' %} selected{% endif %}>{{ lang.ratelimit.hour }}</option>
+          <option value="d"{% if template.attributes.rl_frame == 'd' %} selected{% endif %}>{{ lang.ratelimit.day }}</option>
+        </select>
+        <p class="text-muted mt-3">{{ lang.edit.mbox_rl_info }}</p>
+      </div>
+    </div>
+    <hr>
+    <div class="row my-2">
+      <div class="offset-sm-2 col-sm-10">
+        <select name="active" class="form-control">
+          <option value="1"{% if template.attributes.active == '1' %} selected{% endif %}>{{ lang.edit.active }}</option>
+          <option value="2"{% if template.attributes.active == '2' %} selected{% endif %}>{{ lang.edit.disable_login }}</option>
+          <option value="0"{% if template.attributes.active == '0' %} selected{% endif %}>{{ lang.edit.inactive }}</option>
+        </select>
+      </div>
+    </div>
+    <div class="row">
+      <div class="offset-sm-2 col-sm-10">
+        <div class="checkbox">
+          <label><input type="checkbox" value="1" name="force_pw_update"{% if template.attributes.force_pw_update == '1' %} checked{% endif %}> {{ lang.edit.force_pw_update }}</label>
+          <small class="text-muted">{{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }}</small>
+        </div>
+      </div>
+    </div>
+    {% if not skip_sogo %}
+    <div class="row">
+      <div class="offset-sm-2 col-sm-10">
+        <div class="checkbox">
+          <label><input type="checkbox" value="1" name="sogo_access"{% if template.attributes.sogo_access == '1' %} checked{% endif %}> {{ lang.edit.sogo_access }}</label>
+          <small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
+        </div>
+      </div>
+    </div>
+    {% endif %}
+    <div class="row my-2">
+      <div class="offset-sm-2 col-sm-10">
+        <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="editmailbox_template" data-item="{{ template.id }}" data-api-url='edit/mailbox/template' data-api-attr='{}' href="#">{{ lang.edit.save }}</button>
+      </div>
+    </div>
+  </form>
+</div>
+{% else %}
+  {{ parent() }}
+{% endif %}
+{% endblock %}

+ 7 - 1
data/web/templates/edit/mailbox.twig

@@ -250,11 +250,17 @@
         </div>
       </div>
       {% endif %}
-      <div class="row">
+      <div class="row mb-2">
         <div class="offset-sm-2 col-sm-10">
           <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="editmailbox" data-item="{{ result.username }}" data-api-url='edit/mailbox' data-api-attr='{}' href="#">{{ lang.edit.save }}</button>
         </div>
       </div>
+      <div class="row">
+        <div class="offset-sm-2 col-sm-10">
+          <small class="fst-italic d-block">{{ lang.edit.created_on }}: {{ result.created }}</small>
+          <small class="fst-italic d-block">{{ lang.edit.last_modified }}: {{ result.modified }}</small>
+        </div>
+      </div>
     </form>
   </div>
   <div id="mpushover" class="tab-pane fade" role="tabpanel" aria-labelledby="mailbox-pushover">

+ 6 - 3
data/web/templates/mailbox.twig

@@ -6,8 +6,8 @@
     <li class="nav-item dropdown" role="presentation">
     <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.domains }}</a>
     <ul class="dropdown-menu">
-      <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailboxes" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
-      <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailbox-defaults" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailbox-defaults">Default settings</button></li>
+      <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
+      <li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-domains">{{ lang.mailbox.templates }}</button></li>
     </ul>
     </li>
     {# <li class="nav-item" role="presentation"><button class="nav-link active" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li> #}
@@ -16,7 +16,7 @@
     <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailboxes }}</a>
     <ul class="dropdown-menu">
       <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailboxes" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailboxes">{{ lang.mailbox.mailboxes }}</button></li>
-      <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailbox-defaults" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailbox-defaults">{{ lang.mailbox.mailbox_defaults }}</button></li>
+      <li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-mbox" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-mbox">{{ lang.mailbox.templates }}</button></li>
     </ul>
     </li>
     <li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-resources" role="tab" data-bs-toggle="tab" data-bs-target="#tab-resources">{{ lang.mailbox.resources }}</button></li>
@@ -38,7 +38,9 @@
       <div class="tab-content" style="padding-top:20px">
         {% include 'mailbox/tab-domains.twig' %}
         {# {% include 'mailbox/tab-mailbox-defaults.twig' %} #}
+        {% include 'mailbox/tab-templates-domains.twig' %}
         {% include 'mailbox/tab-mailboxes.twig' %}
+        {% include 'mailbox/tab-templates-mbox.twig' %}
         {% include 'mailbox/tab-resources.twig' %}
         {% include 'mailbox/tab-domain-aliases.twig' %}
         {% include 'mailbox/tab-mbox-aliases.twig' %}
@@ -56,6 +58,7 @@
 <script type='text/javascript'>
   var acl = '{{ acl_json|raw }}';
   var lang = {{ lang_mailbox|raw }};
+  var lang_rl = {{ lang_rl|raw }};
   var lang_datatables = {{ lang_datatables|raw }};
   var csrf_token = '{{ csrf_token }}';
   var pagination_size = '{{ pagination_size }}';

+ 51 - 0
data/web/templates/mailbox/tab-templates-domains.twig

@@ -0,0 +1,51 @@
+<div role="tabpanel" class="tab-pane fade show" id="tab-templates-domains" role="tabpanel" aria-labelledby="tab-templates-domains">
+  <div class="card mb-4">
+    <div class="card-header d-flex fs-5">
+      <button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-templates-domains" data-bs-toggle="collapse" aria-controls="collapse-tab-templates-domains">
+        {{ lang.mailbox.domain_templates }} <span class="badge bg-info table-lines"></span>
+      </button>
+      <span class="d-none d-md-block">{{ lang.mailbox.domain_templates }} <span class="badge bg-info table-lines"></span></span>
+      
+      <div class="btn-group ms-auto d-flex">
+        <button class="btn btn-xs btn-secondary refresh_table" data-draw="draw_templates_domain_table" data-table="templates_domain_table">{{ lang.admin.refresh }}</button>
+      </div>
+    </div>
+    <div id="collapse-tab-templates-domains" class="card-body collapse show" data-bs-parent="#mail-content">  
+      <div class="mass-actions-mailbox mb-4">
+        <div class="btn-group">
+          <button class="btn btn-sm btn-xs-half btn-secondary" id="toggle_multi_select_all" data-id="domain_template" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</button>
+          <button class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</button>
+          <ul class="dropdown-menu">
+            {% if mailcow_cc_role == 'admin' %}
+              <li><a class="dropdown-item" data-action="delete_selected" data-id="domain_template" data-api-url='delete/domain/template' href="#">{{ lang.mailbox.remove }}</a></li>
+              <li><hr class="dropdown-divider"></li>
+              <li><a class="dropdown-item" data-datatables-expand="templates_domain_table">{{ lang.datatables.expand_all }}</a></li>
+              <li><a class="dropdown-item" data-datatables-collapse="templates_domain_table">{{ lang.datatables.collapse_all }}</a></li>
+            {% endif %}
+          </ul>
+          {% if mailcow_cc_role == 'admin' %}
+          <a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addDomainTemplateModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_template }}</a>
+          {% endif %}
+        </div>
+      </div>
+      <table id="templates_domain_table" class="table table-striped dt-responsive w-100"></table>  
+      <div class="mass-actions-mailbox mt-4">
+        <div class="btn-group">
+          <button class="btn btn-sm btn-xs-half btn-secondary" id="toggle_multi_select_all" data-id="domain_template" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</button>
+          <button class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</button>
+          <ul class="dropdown-menu">
+            {% if mailcow_cc_role == 'admin' %}
+              <li><a class="dropdown-item" data-action="delete_selected" data-id="domain_template" data-api-url='delete/domain/template' href="#">{{ lang.mailbox.remove }}</a></li>
+              <li><hr class="dropdown-divider"></li>
+              <li><a class="dropdown-item" data-datatables-expand="templates_domain_table">{{ lang.datatables.expand_all }}</a></li>
+              <li><a class="dropdown-item" data-datatables-collapse="templates_domain_table">{{ lang.datatables.collapse_all }}</a></li>
+            {% endif %}
+          </ul>
+          {% if mailcow_cc_role == 'admin' %}
+          <a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addDomainTemplateModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_template }}</a>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 51 - 0
data/web/templates/mailbox/tab-templates-mbox.twig

@@ -0,0 +1,51 @@
+<div role="tabpanel" class="tab-pane fade show" id="tab-templates-mbox" role="tabpanel" aria-labelledby="tab-templates-mbox">
+  <div class="card mb-4">
+    <div class="card-header d-flex fs-5">
+      <button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-templates-mbox" data-bs-toggle="collapse" aria-controls="collapse-tab-templates-mbox">
+        {{ lang.mailbox.mailbox_templates }} <span class="badge bg-info table-lines"></span>
+      </button>
+      <span class="d-none d-md-block">{{ lang.mailbox.mailbox_templates }} <span class="badge bg-info table-lines"></span></span>
+      
+      <div class="btn-group ms-auto d-flex">
+        <button class="btn btn-xs btn-secondary refresh_table" data-draw="draw_templates_mbox_table" data-table="templates_mbox_table">{{ lang.admin.refresh }}</button>
+      </div>
+    </div>
+    <div id="collapse-tab-templates-mbox" class="card-body collapse show" data-bs-parent="#mail-content">
+      <div class="mass-actions-mailbox mb-4">
+        <div class="btn-group">
+          <button class="btn btn-sm btn-xs-half btn-secondary" id="toggle_multi_select_all" data-id="mailbox_template" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</button>
+          <button class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</button>
+          <ul class="dropdown-menu">
+            {% if mailcow_cc_role == 'admin' %}
+              <li><a class="dropdown-item" data-action="delete_selected" data-id="mailbox_template" data-api-url='delete/mailbox/template' href="#">{{ lang.mailbox.remove }}</a></li>
+              <li><hr class="dropdown-divider"></li>
+              <li><a class="dropdown-item" data-datatables-expand="templates_mbox_table">{{ lang.datatables.expand_all }}</a></li>
+              <li><a class="dropdown-item" data-datatables-collapse="templates_mbox_table">{{ lang.datatables.collapse_all }}</a></li>
+            {% endif %}
+          </ul>
+          {% if mailcow_cc_role == 'admin' %}
+          <a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addMailboxTemplateModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_template }}</a>
+          {% endif %}
+        </div>
+      </div>
+      <table id="templates_mbox_table" class="table table-striped dt-responsive w-100"></table>
+      <div class="mass-actions-mailbox mt-4">
+        <div class="btn-group">
+          <button class="btn btn-sm btn-xs-half btn-secondary" id="toggle_multi_select_all" data-id="mailbox_template" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</button>
+          <button class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</button>
+          <ul class="dropdown-menu">
+            {% if mailcow_cc_role == 'admin' %}
+              <li><a class="dropdown-item" data-action="delete_selected" data-id="mailbox_template" data-api-url='delete/mailbox/template' href="#">{{ lang.mailbox.remove }}</a></li>
+              <li><hr class="dropdown-divider"></li>
+              <li><a class="dropdown-item" data-datatables-expand="templates_mbox_table">{{ lang.datatables.expand_all }}</a></li>
+              <li><a class="dropdown-item" data-datatables-collapse="templates_mbox_table">{{ lang.datatables.collapse_all }}</a></li>
+            {% endif %}
+          </ul>
+          {% if mailcow_cc_role == 'admin' %}
+          <a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addMailboxTemplateModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_template }}</a>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 4 - 4
data/web/templates/modals/footer.twig

@@ -347,12 +347,12 @@
     </div>
   </div>
 </div>
-<!-- whats new modal -->
-<div class="modal fade" id="showWhatsNewModal" tabindex="-1" role="dialog" aria-hidden="true">
+<!-- version modal -->
+<div class="modal fade" id="showVersionModal" tabindex="-1" role="dialog" aria-hidden="true">
   <div class="modal-dialog modal-lg">
     <div class="modal-content">
       <div class="modal-header">
-        <h5 class="modal-title">What's new?</h5>
+        <h5 class="modal-title"></h5>
         <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
       </div>
       <div class="modal-body d-flex flex-column mt-2 p-4">
@@ -362,4 +362,4 @@
       </div>
     </div>
   </div>
-</div><!-- whats new modal -->
+</div><!-- version modal -->

+ 453 - 23
data/web/templates/modals/mailbox.twig

@@ -8,6 +8,10 @@
       </div>
       <div class="modal-body">
         <form class="form-horizontal" data-cached-form="true" data-id="add_mailbox" role="form" autocomplete="off">
+          <input type="hidden" value="0" name="force_pw_update">
+          <input type="hidden" value="0" name="sogo_access">
+          <input type="hidden" value="0" name="protocol_access">
+
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="local_part">{{ lang.add.mailbox_username }}</label>
             <div class="col-sm-10">
@@ -30,19 +34,38 @@
               <input type="text" class="form-control" name="name">
             </div>
           </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password">{{ lang.add.password }} (<a href="#" class="generate_password">{{ lang.add.generate }}</a>)</label>
+            <div class="col-sm-10">
+              <input type="password" data-pwgen-field="true" data-hibp="true" class="form-control" name="password" placeholder="" autocomplete="new-password" required>
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password2">{{ lang.add.password_repeat }}</label>
+            <div class="col-sm-10">
+              <input type="password" data-pwgen-field="true" class="form-control" name="password2" placeholder="" autocomplete="new-password" required>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="description">{{ lang.mailbox.template }}</label>
+            <div class="col-sm-10">
+              <select data-live-search="true" id="mailbox_templates" class="form-control" title="{{ lang.mailbox.template }}">
+              </select>
+            </div>
+          </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.tags }}</label>
             <div class="col-sm-10">
               <div class="form-control tag-box">
-                <input type="text" class="tag-input">
+                <input type="text" class="tag-input" id="addMailbox_tags">
                 <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
                 <input type="hidden" value="" name="tags" class="tag-values" />
               </div>
             </div>
           </div>
-          <div class="row mb-2">
+          <div class="row mb-4">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="addInputQuota">{{ lang.add.quota_mb }}
-              <br /><span id="quotaBadge" class="badge">max. - MiB</span>
+              <br /><span id="quotaBadge" class="badge bg-primary">max. - MiB</span>
             </label>
             <div class="col-sm-10">
               <input type="text" class="form-control" name="quota" min="0" max="" id="addInputQuota" disabled value="{{ lang.add.select_domain }}" required>
@@ -51,26 +74,124 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password">{{ lang.add.password }} (<a href="#" class="generate_password">{{ lang.add.generate }}</a>)</label>
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.user.quarantine_notification }}</label>
             <div class="col-sm-10">
-              <input type="password" data-pwgen-field="true" data-hibp="true" class="form-control" name="password" placeholder="" autocomplete="new-password" required>
+              <div class="btn-group">
+                <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_never" autocomplete="off" value="never">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_never">{{ lang.user.never }}</label>
+
+                <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_hourly" autocomplete="off" value="hourly">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_hourly">{{ lang.user.hourly }}</label>
+
+                <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_daily" autocomplete="off" value="daily">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_daily">{{ lang.user.daily }}</label>
+
+                <input type="radio" class="btn-check" name="quarantine_notification" id="quarantine_notification_weekly" autocomplete="off" value="weekly">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_notification_weekly">{{ lang.user.weekly }}</label>
+              </div>
+              <p class="text-muted"><small>{{ lang.user.quarantine_notification_info }}</small></p>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.user.quarantine_category }}</label>
+            <div class="col-sm-10">
+              <div class="btn-group">
+                <input type="radio" class="btn-check" name="quarantine_category" id="quarantine_category_reject" autocomplete="off" value="reject">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_category_reject">{{ lang.user.q_reject }}</label>
+                
+                <input type="radio" class="btn-check" name="quarantine_category" id="quarantine_category_add_header" autocomplete="off" value="add_header">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_category_add_header">{{ lang.user.q_add_header }}</label>
+                
+                <input type="radio" class="btn-check" name="quarantine_category" id="quarantine_category_all" autocomplete="off" value="all">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="quarantine_category_all">{{ lang.user.q_all }}</label>
+              </div>
+              <p class="text-muted"><small>{{ lang.user.quarantine_category_info }}</small></p>
             </div>
           </div>
           <div class="row mb-4">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password2">{{ lang.add.password_repeat }}</label>
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="tls_enforce_in">{{ lang.user.tls_policy }}</label>
             <div class="col-sm-10">
-              <input type="password" data-pwgen-field="true" class="form-control" name="password2" placeholder="" autocomplete="new-password" required>
+              <div class="btn-group">
+                <input type="checkbox" class="btn-check" name="tls_enforce_in" id="tls_enforce_in" autocomplete="off" value="1">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="tls_enforce_in">{{ lang.user.tls_enforce_in }}</label>
+                
+                <input type="checkbox" class="btn-check" name="tls_enforce_out" id="tls_enforce_out" autocomplete="off" value="1">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="tls_enforce_out">{{ lang.user.tls_enforce_out }}</label>
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="protocol_access">{{ lang.edit.allowed_protocols }}</label>
+            <div class="col-sm-10">
+              <select name="protocol_access" id="protocol_access" multiple class="form-control">
+                <option value="imap">IMAP</option>
+                <option value="pop3">POP3</option>
+                <option value="smtp">SMTP</option>
+                <option value="sieve">Sieve</option>
+              </select>
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">ACL</label>
+            <div class="col-sm-10">
+              <select id="user_acl" name="acl" multiple class="form-control">
+                  <option value="spam_alias" selected>{{ lang.acl["spam_alias"] }}</option>
+                  <option value="tls_policy" selected>{{ lang.acl["tls_policy"] }}</option>
+                  <option value="spam_score" selected>{{ lang.acl["spam_score"] }}</option>
+                  <option value="spam_policy" selected>{{ lang.acl["spam_policy"] }}</option>
+                  <option value="delimiter_action" selected>{{ lang.acl["delimiter_action"] }}</option>
+                  <option value="syncjobs">{{ lang.acl["syncjobs"] }}</option>
+                  <option value="eas_reset" selected>{{ lang.acl["eas_reset"] }}</option>
+                  <option value="sogo_profile_reset">{{ lang.acl["sogo_profile_reset"] }}</option>
+                  <option value="pushover" selected>{{ lang.acl["pushover"] }}</option>
+                  <option value="quarantine" selected>{{ lang.acl["quarantine"] }}</option>
+                  <option value="quarantine_attachments" selected>{{ lang.acl["quarantine_attachments"] }}</option>
+                  <option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
+                  <option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
+                  <option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
+              </select>
             </div>
           </div>
           <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.acl.ratelimit }}</label>
+            <div class="col-sm-10">
+              <input name="rl_value" id="rl_value" type="number" autocomplete="off" value="" class="form-control mb-2" placeholder="{{ lang.ratelimit.disabled }}">
+              <select name="rl_frame" id="rl_frame" class="form-control">
+                <option value="s">{{ lang.ratelimit.second }}</option>
+                <option value="m">{{ lang.ratelimit.minute }}</option>
+                <option value="h">{{ lang.ratelimit.hour }}</option>
+                <option value="d">{{ lang.ratelimit.day }}</option>
+              </select>
+              <p class="text-muted mt-3">{{ lang.edit.mbox_rl_info }}</p>
+            </div>
+          </div>
+          <div class="row mb-2">
             <div class="offset-sm-2 col-sm-10">
-              <select name="active" class="form-control">
+              <select name="active" id="mbox_active" class="form-control">
                 <option value="1" selected>{{ lang.add.active }}</option>
                 <option value="2">{{ lang.add.disable_login }}</option>
                 <option value="0">{{ lang.add.inactive }}</option>
               </select>
             </div>
           </div>
+          <div class="row">
+            <div class="offset-sm-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="force_pw_update" id="force_pw_update"> {{ lang.edit.force_pw_update }}</label>
+                <small class="text-muted">{{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }}</small>
+              </div>
+            </div>
+          </div>
+          {% if not skip_sogo %}
+          <div class="row">
+            <div class="offset-sm-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="sogo_access" id="sogo_access"> {{ lang.edit.sogo_access }}</label>
+                <small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
+              </div>
+            </div>
+          </div>
+          {% endif %}
           <hr>
           <div class="row">
             <div class="offset-sm-2 col-sm-10">
@@ -82,6 +203,176 @@
     </div>
   </div>
 </div><!-- add mailbox modal -->
+<!-- add mailbox template modal -->
+<div class="modal fade" id="addMailboxTemplateModal" tabindex="-1" role="dialog" aria-hidden="true">
+  <div class="modal-dialog modal-xl">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3 class="modal-title">{{ lang.mailbox.add_template }}</h3>
+        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+      </div>
+      <div class="modal-body">
+        <form class="form-horizontal" data-id="addmailbox_template" role="form" method="post">
+          <input type="hidden" value="default" name="sender_acl">
+          <input type="hidden" value="0" name="force_pw_update">
+          <input type="hidden" value="0" name="sogo_access">
+          <input type="hidden" value="0" name="protocol_access">     
+
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="template">{{ lang.mailbox.template }}</label>
+            <div class="col-sm-10">
+              <div class="input-group mb-3">
+                <input type="text" name="template" class="form-control" aria-label="Text input with dropdown button" value="" />
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.tags }}</label>
+            <div class="col-sm-10">
+              <div class="form-control tag-box">
+                <input type="text" class="tag-input" id="addMailbox_tags">
+                <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+                <input type="hidden" value="" name="tags" class="tag-values" />
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="quota">{{ lang.edit.quota_mb }}</label>
+            <div class="col-sm-10">
+              <input type="number" name="quota" style="width:100%" min="0" value="" class="form-control">
+              <small class="text-muted">0 = ∞</small>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.user.quarantine_notification }}</label>
+            <div class="col-sm-10">
+              <div class="btn-group">
+                <input type="radio" class="btn-check" name="quarantine_notification" id="template_quarantine_notification_never" autocomplete="off" value="never">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_notification_never">{{ lang.user.never }}</label>
+
+                <input type="radio" class="btn-check" name="quarantine_notification" id="template_quarantine_notification_hourly" autocomplete="off" value="hourly">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_notification_hourly">{{ lang.user.hourly }}</label>
+
+                <input type="radio" class="btn-check" name="quarantine_notification" id="template_quarantine_notification_daily" autocomplete="off" value="daily">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_notification_daily">{{ lang.user.daily }}</label>
+
+                <input type="radio" class="btn-check" name="quarantine_notification" id="template_quarantine_notification_weekly" autocomplete="off" value="weekly">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_notification_weekly">{{ lang.user.weekly }}</label>
+              </div>
+              <p class="text-muted"><small>{{ lang.user.quarantine_notification_info }}</small></p>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.user.quarantine_category }}</label>
+            <div class="col-sm-10">
+              <div class="btn-group">
+                <input type="radio" class="btn-check" name="quarantine_category" id="template_quarantine_category_reject" autocomplete="off" value="reject" >
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_category_reject">{{ lang.user.q_reject }}</label>
+                
+                <input type="radio" class="btn-check" name="quarantine_category" id="template_quarantine_category_add_header" autocomplete="off" value="add_header">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_category_add_header">{{ lang.user.q_add_header }}</label>
+                
+                <input type="radio" class="btn-check" name="quarantine_category" id="template_quarantine_category_all" autocomplete="off" value="all">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_quarantine_category_all">{{ lang.user.q_all }}</label>
+              </div>
+              <p class="text-muted"><small>{{ lang.user.quarantine_category_info }}</small></p>
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="sender_acl">{{ lang.user.tls_policy }}</label>
+            <div class="col-sm-10">
+              <div class="btn-group">
+                <input type="checkbox" class="btn-check" name="tls_enforce_in" id="template_tls_enforce_in" autocomplete="off" value="1">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_tls_enforce_in">{{ lang.user.tls_enforce_in }}</label>
+                
+                <input type="checkbox" class="btn-check" name="tls_enforce_out" id="template_tls_enforce_out" autocomplete="off" value="1">
+                <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="template_tls_enforce_out">{{ lang.user.tls_enforce_out }}</label>
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="protocol_access">{{ lang.edit.allowed_protocols }}</label>
+            <div class="col-sm-10">
+              <select name="protocol_access" multiple class="form-control">
+                <option value="imap" selected>IMAP</option>
+                <option value="pop3" selected>POP3</option>
+                <option value="smtp" selected>SMTP</option>
+                <option value="sieve" selected>Sieve</option>
+              </select>
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">ACL</label>
+            <div class="col-sm-10">
+              <select id="template_user_acl" name="acl" size="10" multiple class="form-control">                  
+                <option value="spam_alias" selected>{{ lang.acl["spam_alias"] }}</option>
+                <option value="tls_policy" selected>{{ lang.acl["tls_policy"] }}</option>
+                <option value="spam_score" selected>{{ lang.acl["spam_score"] }}</option>
+                <option value="spam_policy" selected>{{ lang.acl["spam_policy"] }}</option>
+                <option value="delimiter_action" selected>{{ lang.acl["delimiter_action"] }}</option>
+                <option value="syncjobs">{{ lang.acl["syncjobs"] }}</option>
+                <option value="eas_reset" selected>{{ lang.acl["eas_reset"] }}</option>
+                <option value="sogo_profile_reset">{{ lang.acl["sogo_profile_reset"] }}</option>
+                <option value="pushover" selected>{{ lang.acl["pushover"] }}</option>
+                <option value="quarantine" selected>{{ lang.acl["quarantine"] }}</option>
+                <option value="quarantine_attachments" selected>{{ lang.acl["quarantine_attachments"] }}</option>
+                <option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
+                <option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
+                <option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
+              </select>
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.acl.ratelimit }}</label>
+            <div class="col-sm-10">
+              <input name="rl_value" type="number" autocomplete="off" value="" class="form-control mb-2" placeholder="{{ lang.ratelimit.disabled }}">
+              <select name="rl_frame" class="form-control">
+                <option value="s">{{ lang.ratelimit.second }}</option>
+                <option value="m">{{ lang.ratelimit.minute }}</option>
+                <option value="h">{{ lang.ratelimit.hour }}</option>
+                <option value="d">{{ lang.ratelimit.day }}</option>
+              </select>
+              <p class="text-muted mt-3">{{ lang.edit.mbox_rl_info }}</p>
+            </div>
+          </div>
+          <hr>
+          <div class="row my-2">
+            <div class="offset-sm-2 col-sm-10">
+              <select id="mbox_acitve" name="active" class="form-control">
+                <option value="1" selected>{{ lang.edit.active }}</option>
+                <option value="2">{{ lang.edit.disable_login }}</option>
+                <option value="0">{{ lang.edit.inactive }}</option>
+              </select>
+            </div>
+          </div>
+          <div class="row">
+            <div class="offset-sm-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="force_pw_update"> {{ lang.edit.force_pw_update }}</label>
+                <small class="text-muted">{{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }}</small>
+              </div>
+            </div>
+          </div>
+          {% if not skip_sogo %}
+          <div class="row">
+            <div class="offset-sm-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="sogo_access"> {{ lang.edit.sogo_access }}</label>
+                <small class="text-muted">{{ lang.edit.sogo_access_info }}</small>
+              </div>
+            </div>
+          </div>
+          {% endif %}
+          <div class="row my-2">
+            <div class="offset-sm-2 col-sm-10">
+              <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="add_item" data-id="addmailbox_template" data-api-url='add/mailbox/template' data-api-attr='{}' href="#">{{ lang.admin.add }}</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div><!-- add mailbox template modal -->
 <!-- add domain modal -->
 <div class="modal fade" id="addDomainModal" tabindex="-1" role="dialog" aria-hidden="true">
   <div class="modal-dialog modal-xl">
@@ -104,11 +395,18 @@
               <input type="text" class="form-control" name="description">
             </div>
           </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="description">{{ lang.mailbox.template }}</label>
+            <div class="col-sm-10">
+              <select data-live-search="true" id="domain_templates" class="form-control">
+              </select>
+            </div>
+          </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.tags }}</label>
             <div class="col-sm-10">
               <div class="form-control tag-box">
-                <input type="text" class="tag-input">
+                <input type="text" class="tag-input" id="addDomain_tags">
                 <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
                 <input type="hidden" value="" name="tags" class="tag-values" />
               </div>
@@ -117,38 +415,38 @@
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="aliases">{{ lang.add.max_aliases }}</label>
             <div class="col-sm-10">
-              <input type="number" class="form-control" name="aliases" value="400" required>
+              <input type="number" id="addDomain_max_aliases" class="form-control" name="aliases" value="400" required>
             </div>
           </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="mailboxes">{{ lang.add.max_mailboxes }}</label>
             <div class="col-sm-10">
-              <input type="number" class="form-control" name="mailboxes" value="10" required>
+              <input type="number" id="addDomain_max_mailboxes" class="form-control" name="mailboxes" value="10" required>
             </div>
           </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="defquota">{{ lang.add.mailbox_quota_def }}</label>
             <div class="col-sm-10">
-              <input type="number" class="form-control" name="defquota" value="3072" required>
+              <input type="number" id="addDomain_mailbox_quota_def" class="form-control" name="defquota" value="3072" required>
             </div>
           </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="maxquota">{{ lang.add.mailbox_quota_m }}</label>
             <div class="col-sm-10">
-              <input type="number" class="form-control" name="maxquota" value="10240" required>
+              <input type="number" id="addDomain_mailbox_quota_m" class="form-control" name="maxquota" value="10240" required>
             </div>
           </div>
           <div class="row mb-4">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="quota">{{ lang.add.domain_quota_m }}</label>
             <div class="col-sm-10">
-              <input type="number" class="form-control" name="quota" value="10240" required>
+              <input type="number" id="addDomain_domain_quota_m" class="form-control" name="quota" value="10240" required>
             </div>
           </div>
           {% if not skip_sogo %}
           <div class="row mb-2">
             <div class="offset-sm-2 col-sm-10">
               <div class="checkbox">
-                <label><input type="checkbox" value="1" name="gal" checked> {{ lang.edit.gal }}</label>
+                <label><input type="checkbox" id="addDomain_gal" value="1" name="gal" checked> {{ lang.edit.gal }}</label>
                 <small class="text-muted">{{ lang.edit.gal_info|raw }}</small>
               </div>
             </div>
@@ -157,7 +455,7 @@
           <div class="row mb-4">
             <div class="offset-sm-2 col-sm-10">
               <div class="checkbox">
-                <label><input type="checkbox" value="1" name="active" checked> {{ lang.add.active }}</label>
+                <label><input type="checkbox" id="addDomain_active" value="1" name="active" checked> {{ lang.add.active }}</label>
               </div>
             </div>
           </div>
@@ -165,10 +463,10 @@
           <div class="row mb-4">
             <label class="control-label col-sm-2 text-sm-end" for="rl_frame">{{ lang.acl.ratelimit }}</label>
             <div class="col-sm-7">
-              <input name="rl_value" type="number" class="form-control" placeholder="{{ lang.ratelimit.disabled }}">
+              <input name="rl_value" id="addDomain_rl_value" type="number" class="form-control" placeholder="{{ lang.ratelimit.disabled }}">
             </div>
             <div class="col-sm-3">
-              <select name="rl_frame" class="form-control">
+              <select name="rl_frame" id="addDomain_rl_frame" class="form-control">
               {% include 'mailbox/rl-frame.twig' %}
               </select>
             </div>
@@ -184,8 +482,8 @@
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="key_size">{{ lang.admin.dkim_key_length }}</label>
             <div class="col-sm-10">
               <select data-style="btn btn-secondary btn-sm" class="form-control" id="key_size" name="key_size">
-                <option data-subtext="bits">1024</option>
-                <option data-subtext="bits" selected>2048</option>
+                <option data-subtext="bits" value="1024">1024</option>
+                <option data-subtext="bits" value="2048" selected>2048</option>
               </select>
             </div>
           </div>
@@ -194,11 +492,11 @@
             <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.backup_mx_options }}</label>
             <div class="col-sm-10">
               <div class="checkbox">
-                <label><input type="checkbox" value="1" name="backupmx"> {{ lang.add.relay_domain }}</label>
+                <label><input type="checkbox" id="addDomain_relay_domain" value="1" name="backupmx"> {{ lang.add.relay_domain }}</label>
                 <br>
-                <label><input type="checkbox" value="1" name="relay_all_recipients"> {{ lang.add.relay_all }}</label>
+                <label><input type="checkbox" id="addDomain_relay_all" value="1" name="relay_all_recipients"> {{ lang.add.relay_all }}</label>
                 <p>{{ lang.add.relay_all_info|raw }}</p>
-                <label><input type="checkbox" value="1" name="relay_unknown_only"> {{ lang.add.relay_unknown_only }}</label>
+                <label><input type="checkbox" id="addDomain_relay_unknown_only" value="1" name="relay_unknown_only"> {{ lang.add.relay_unknown_only }}</label>
                 <br>
                 <p>{{ lang.add.relay_transport_info|raw }}</p>
               </div>
@@ -225,6 +523,138 @@
     </div>
   </div>
 </div><!-- add domain modal -->
+<!-- add domain template modal -->
+<div class="modal fade" id="addDomainTemplateModal" tabindex="-1" role="dialog" aria-hidden="true">
+  <div class="modal-dialog modal-xl">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3 class="modal-title">{{ lang.mailbox.add_template }}</h3>
+        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+      </div>
+      <div class="modal-body">
+        <form data-id="adddomain_template" class="form-horizontal" role="form" method="post">
+          {% if mailcow_cc_role == 'admin' %}
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="template">{{ lang.mailbox.template }}</label>
+            <div class="col-sm-10">
+              <div class="input-group mb-3">
+                <input type="text" name="template" class="form-control" aria-label="Text input with dropdown button" value="" />
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.tags }}</label>
+            <div class="col-sm-10">
+              <div class="form-control tag-box">
+                <input type="text" class="tag-input">
+                <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+                <input type="hidden" value="" name="tags" class="tag-values" />
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="max_num_aliases_for_domain">{{ lang.add.max_aliases }}</label>
+            <div class="col-sm-10">
+              <input type="number" class="form-control" name="max_num_aliases_for_domain" value="">
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="max_num_mboxes_for_domain">{{ lang.add.max_mailboxes }}</label>
+            <div class="col-sm-10">
+              <input type="number" class="form-control" name="max_num_mboxes_for_domain" value="">
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="def_quota_for_mbox">{{ lang.add.mailbox_quota_def }}</label>
+            <div class="col-sm-10">
+              <input type="number" class="form-control" name="def_quota_for_mbox" value="">
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="max_quota_for_mbox">{{ lang.add.mailbox_quota_m }}</label>
+            <div class="col-sm-10">
+              <input type="number" class="form-control" name="max_quota_for_mbox" value="">
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="max_quota_for_domain">{{ lang.add.domain_quota_m }}</label>
+            <div class="col-sm-10">
+              <input type="number" class="form-control" name="max_quota_for_domain" value="">
+            </div>
+          </div>
+          <div class="row">
+            <div class="offset-sm-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="gal" checked> {{ lang.add.gal }}</label>
+                <small class="text-muted">{{ lang.add.gal_info|raw }}</small>
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <div class="offset-sm-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="active" checked> {{ lang.add.active }}</label>
+              </div>
+            </div>
+          </div>
+          <hr>
+          <div class="row">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.edit.ratelimit }}</label>
+            <div class="col-sm-7">
+              <input name="rl_value" type="number" value="" autocomplete="off" class="form-control mb-4" placeholder="{{ lang.ratelimit.disabled }}">
+            </div>
+            <div class="col-sm-3">
+              <select name="rl_frame" class="form-control">
+                <option value="s">{{ lang.ratelimit.second }}</option>
+                <option value="m">{{ lang.ratelimit.minute }}</option>
+                <option value="h">{{ lang.ratelimit.hour }}</option>
+                <option value="d">{{ lang.ratelimit.day }}</option>
+              </select>
+            </div>
+          </div>
+          {% endif %}
+          <hr>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="dkim_selector">{{ lang.admin.dkim_domains_selector }}</label>
+            <div class="col-sm-10">
+              <input class="form-control" id="dkim_selector" name="dkim_selector" value="dkim">
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="key_size">{{ lang.admin.dkim_key_length }}</label>
+            <div class="col-sm-10">
+              <select data-style="btn btn-secondary btn-sm" class="form-control" id="key_size" name="key_size">
+                <option data-subtext="bits">1024</option>
+                <option data-subtext="bits" selected>2048</option>
+              </select>
+            </div>
+          </div>
+          <hr>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.edit.backup_mx_options }}</label>
+            <div class="col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" value="1" name="backupmx"> {{ lang.edit.relay_domain }}</label>
+                <br>
+                <label><input type="checkbox" value="1" name="relay_all_recipients"> {{ lang.edit.relay_all }}</label>
+                <p>{{ lang.edit.relay_all_info|raw }}</p>
+                <label><input type="checkbox" value="1" name="relay_unknown_only"> {{ lang.edit.relay_unknown_only }}</label>
+                <br>
+                <p>{{ lang.edit.relay_transport_info|raw }}</p>
+              </div>
+            </div>
+          </div>
+          <hr>
+          <div class="row">
+            <div class="offset-sm-2 col-sm-10">
+              <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="add_item" data-id="adddomain_template" data-item="{{ domain }}" data-api-url='add/domain/template' data-api-attr='{}' href="#">{{ lang.admin.add }}</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div><!-- add domain template modal -->
 <!-- add resource modal -->
 <div class="modal fade" id="addResourceModal" tabindex="-1" role="dialog" aria-hidden="true">
   <div class="modal-dialog modal-xl">