Browse Source

[Web] Implement XMPP
[Web] Various small fixes and enhancements

andryyy 4 years ago
parent
commit
06c89bac7d

+ 210 - 159
data/web/edit.php

@@ -256,184 +256,216 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         $rlyhosts = relayhost('get');
         $rlyhosts = relayhost('get');
         if (!empty($result)) {
         if (!empty($result)) {
         ?>
         ?>
-          <h4><?=$lang['edit']['domain'];?></h4>
-          <form data-id="editdomain" 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">
-            <div class="form-group" data-acl="<?=$_SESSION['acl']['domain_desc'];?>">
-              <label class="control-label col-sm-2" for="description"><?=$lang['edit']['description'];?></label>
-              <div class="col-sm-10">
-                <input type="text" class="form-control" name="description" value="<?=htmlspecialchars($result['description']);?>">
+          <ul class="nav nav-tabs">
+            <li class="active"><a data-toggle="tab" href="#dedit"><?=$lang['edit']['domain'];?></a></li>
+            <li><a data-toggle="tab" href="#dratelimit"><?=$lang['edit']['ratelimit'];?></a></li>
+            <li><a data-toggle="tab" href="#dspamfilter"><?=$lang['edit']['spam_filter'];?></a></li>
+          </ul>
+          <hr>
+          <div class="tab-content">
+            <div id="dedit" class="tab-pane in active">
+            <form data-id="editdomain" 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="xmpp">
+              <input type="hidden" value="0" name="relay_all_recipients">
+              <input type="hidden" value="0" name="relay_unknown_only">
+              <div class="form-group" data-acl="<?=$_SESSION['acl']['domain_desc'];?>">
+                <label class="control-label col-sm-2" for="description"><?=$lang['edit']['description'];?></label>
+                <div class="col-sm-10">
+                  <input type="text" class="form-control" name="description" value="<?=htmlspecialchars($result['description']);?>">
+                </div>
               </div>
               </div>
-            </div>
-            <?php
-            if ($_SESSION['mailcow_cc_role'] == "admin") {
-            ?>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="aliases"><?=$lang['edit']['max_aliases'];?></label>
-              <div class="col-sm-10">
-                <input type="number" class="form-control" name="aliases" value="<?=intval($result['max_num_aliases_for_domain']);?>">
+              <?php
+              if ($_SESSION['mailcow_cc_role'] == "admin") {
+              ?>
+              <div class="form-group">
+                <label class="control-label col-sm-2" for="aliases"><?=$lang['edit']['max_aliases'];?></label>
+                <div class="col-sm-10">
+                  <input type="number" class="form-control" name="aliases" value="<?=intval($result['max_num_aliases_for_domain']);?>">
+                </div>
               </div>
               </div>
-            </div>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="mailboxes"><?=$lang['edit']['max_mailboxes'];?></label>
-              <div class="col-sm-10">
-                <input type="number" class="form-control" name="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
+              <div class="form-group">
+                <label class="control-label col-sm-2" for="mailboxes"><?=$lang['edit']['max_mailboxes'];?></label>
+                <div class="col-sm-10">
+                  <input type="number" class="form-control" name="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
+                </div>
               </div>
               </div>
-            </div>
-            <div class="form-group">
-                <label class="control-label col-sm-2" for="defquota"><?=$lang['edit']['mailbox_quota_def'];?></label>
+              <div class="form-group">
+                  <label class="control-label col-sm-2" for="defquota"><?=$lang['edit']['mailbox_quota_def'];?></label>
+                  <div class="col-sm-10">
+                      <input type="number" class="form-control" name="defquota" value="<?=intval($result['def_quota_for_mbox'] / 1048576);?>">
+                  </div>
+              </div>
+              <div class="form-group">
+                <label class="control-label col-sm-2" for="maxquota"><?=$lang['edit']['max_quota'];?></label>
                 <div class="col-sm-10">
                 <div class="col-sm-10">
-                    <input type="number" class="form-control" name="defquota" value="<?=intval($result['def_quota_for_mbox'] / 1048576);?>">
+                  <input type="number" class="form-control" name="maxquota" value="<?=intval($result['max_quota_for_mbox'] / 1048576);?>">
                 </div>
                 </div>
-            </div>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="maxquota"><?=$lang['edit']['max_quota'];?></label>
-              <div class="col-sm-10">
-                <input type="number" class="form-control" name="maxquota" value="<?=intval($result['max_quota_for_mbox'] / 1048576);?>">
               </div>
               </div>
-            </div>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['domain_quota'];?></label>
-              <div class="col-sm-10">
-                <input type="number" class="form-control" name="quota" value="<?=intval($result['max_quota_for_domain'] / 1048576);?>">
+              <div class="form-group">
+                <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['domain_quota'];?></label>
+                <div class="col-sm-10">
+                  <input type="number" class="form-control" name="quota" value="<?=intval($result['max_quota_for_domain'] / 1048576);?>">
+                </div>
               </div>
               </div>
-            </div>
-            <div class="form-group">
-              <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['relayhost'];?></label>
-              <div class="col-sm-10">
-                <select data-live-search="true" name="relayhost" class="form-control">
-                  <?php
-                  foreach ($rlyhosts as $rlyhost) {
-                  ?>
-                  <option value="<?=$rlyhost['id'];?>" <?=($result['relayhost'] == $rlyhost['id']) ? 'selected' : null;?>>ID <?=$rlyhost['id'];?>: <?=$rlyhost['hostname'];?> (<?=$rlyhost['username'];?>)</option>
-                  <?php
-                  }
-                  ?>
-                  <option value="" <?=($result['relayhost'] == "0") ? 'selected' : null;?>>None</option>
-                </select>
+              <div class="form-group">
+                <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['relayhost'];?></label>
+                <div class="col-sm-10">
+                  <select data-live-search="true" name="relayhost" class="form-control">
+                    <?php
+                    foreach ($rlyhosts as $rlyhost) {
+                    ?>
+                    <option value="<?=$rlyhost['id'];?>" <?=($result['relayhost'] == $rlyhost['id']) ? 'selected' : null;?>>ID <?=$rlyhost['id'];?>: <?=$rlyhost['hostname'];?> (<?=$rlyhost['username'];?>)</option>
+                    <?php
+                    }
+                    ?>
+                    <option value="" <?=($result['relayhost'] == "0") ? 'selected' : null;?>>None</option>
+                  </select>
+                </div>
               </div>
               </div>
-            </div>
-            <div class="form-group">
-              <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" <?=(isset($result['backupmx']) && $result['backupmx']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_domain'];?></label>
-                  <br>
-                  <label><input type="checkbox" value="1" name="relay_all_recipients" <?=(isset($result['relay_all_recipients']) && $result['relay_all_recipients']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_all'];?></label>
-                  <p><?=$lang['edit']['relay_all_info'];?></p>
-                  <label><input type="checkbox" value="1" name="relay_unknown_only" <?=(isset($result['relay_unknown_only']) && $result['relay_unknown_only']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_unknown_only'];?></label>
-                  <br>
-                  <p><?=$lang['edit']['relay_transport_info'];?></p>
-                  <hr style="margin:25px 0px 0px 0px">
+              <div class="form-group">
+                <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" <?=(isset($result['backupmx']) && $result['backupmx']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_domain'];?></label>
+                    <br>
+                    <label><input type="checkbox" value="1" name="relay_all_recipients" <?=(isset($result['relay_all_recipients']) && $result['relay_all_recipients']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_all'];?></label>
+                    <p><?=$lang['edit']['relay_all_info'];?></p>
+                    <label><input type="checkbox" value="1" name="relay_unknown_only" <?=(isset($result['relay_unknown_only']) && $result['relay_unknown_only']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_unknown_only'];?></label>
+                    <br>
+                    <p><?=$lang['edit']['relay_transport_info'];?></p>
+                    <hr style="margin:25px 0px 0px 0px">
+                  </div>
                 </div>
                 </div>
               </div>
               </div>
-            </div>
-            <?php
-            }
-            ?>
-            <div class="form-group">
-              <div class="col-sm-offset-2 col-sm-10">
-                <div class="checkbox">
-                  <label><input type="checkbox" value="1" name="gal" <?=(isset($result['gal']) && $result['gal']=="1") ? "checked" : null;?>> <?=$lang['edit']['gal'];?></label>
-                  <small class="help-block"><?=$lang['edit']['gal_info'];?></small>
+              <?php
+              }
+              ?>
+              <div class="form-group">
+                <div class="col-sm-offset-2 col-sm-10">
+                  <div class="checkbox">
+                    <label><input type="checkbox" value="1" name="gal" <?=(isset($result['gal']) && $result['gal']=="1") ? "checked" : null;?>> <?=$lang['edit']['gal'];?></label>
+                    <small class="help-block"><?=$lang['edit']['gal_info'];?></small>
+                  </div>
                 </div>
                 </div>
               </div>
               </div>
-            </div>
-            <div class="form-group">
-              <div class="col-sm-offset-2 col-sm-10">
-                <div class="checkbox">
-                  <label><input type="checkbox" value="1" name="active" <?=(isset($result['active']) && $result['active']=="1") ? "checked" : null;?> <?=($_SESSION['mailcow_cc_role'] == "admin") ? null : "disabled";?>> <?=$lang['edit']['active'];?></label>
+              <hr>
+              <div class="form-group" data-acl="<?=$_SESSION['acl']['xmpp_mailbox_access'];?>">
+                <div class="col-sm-offset-2 col-sm-10">
+                  <div class="checkbox">
+                    <label><input type="checkbox" value="1" name="xmpp" <?=(isset($result['xmpp']) && $result['xmpp']=="1") ? "checked" : null;?>> <?=$lang['edit']['xmpp'];?></label>
+                    <small class="help-block"><?=$lang['edit']['xmpp_info'];?></small>
+                  </div>
                 </div>
                 </div>
               </div>
               </div>
-            </div>
-            <div class="form-group">
-              <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn 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 class="form-group" data-acl="<?=$_SESSION['acl']['xmpp_prefix'];?>">
+                <label class="control-label col-sm-2" for="xmpp_prefix"><?=$lang['edit']['xmpp_prefix'];?></label>
+                <div class="col-md-3">
+                  <div class="input-group">
+                    <input type="text" class="form-control" name="xmpp_prefix" value="<?=htmlspecialchars($result['xmpp_prefix'], ENT_QUOTES, 'UTF-8');?>" required>
+                    <span class="input-group-addon">.<?=htmlspecialchars($domain, ENT_QUOTES, 'UTF-8');?></span>
+                  </div>
+                  <small class="help-block"><?=sprintf($lang['edit']['xmpp_prefix_info'], getenv('MAILCOW_HOSTNAME'));?></small>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group">
+                <div class="col-sm-offset-2 col-sm-10">
+                  <div class="checkbox">
+                    <label><input type="checkbox" value="1" name="active" <?=(isset($result['active']) && $result['active']=="1") ? "checked" : null;?> <?=($_SESSION['mailcow_cc_role'] == "admin") ? null : "disabled";?>> <?=$lang['edit']['active'];?></label>
+                  </div>
+                </div>
+              </div>
+              <div class="form-group">
+                <div class="col-sm-offset-2 col-sm-10">
+                  <button class="btn btn-success" 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>
+            </form>
+            <?php
+            if (!empty($dkim = dkim('details', $domain))) {
+            ?>
+            <hr>
+            <div class="row">
+              <div class="col-xs-2">
+                <p>Domain: <strong><?=htmlspecialchars($result['domain_name']);?></strong> (<?=$dkim['dkim_selector'];?>._domainkey)</p>
+              </div>
+              <div class="col-xs-10">
+                <pre><?=$dkim['dkim_txt'];?></pre>
               </div>
               </div>
             </div>
             </div>
-          </form>
-          <?php
-          if (!empty($dkim = dkim('details', $domain))) {
-          ?>
-          <hr>
-          <div class="row">
-            <div class="col-xs-2">
-              <p>Domain: <strong><?=htmlspecialchars($result['domain_name']);?></strong> (<?=$dkim['dkim_selector'];?>._domainkey)</p>
-            </div>
-            <div class="col-xs-10">
-              <pre><?=$dkim['dkim_txt'];?></pre>
-            </div>
-          </div>
-          <?php
-          }
-          ?>
-      <hr>
-      <form data-id="domratelimit" class="form-inline well" method="post">
-        <div class="form-group">
-          <label class="control-label"><?=$lang['acl']['ratelimit'];?></label>
-          <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" autocomplete="off" class="form-control" placeholder="disabled">
-        </div>
-        <div class="form-group">
-          <select name="rl_frame" class="form-control">
-            <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
-            <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
-            <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
-            <option value="d" <?=(isset($rl['frame']) && $rl['frame'] == 'd') ? 'selected' : null;?>>msgs / day</option>
-          </select>
-        </div>
-        <div class="form-group">
-          <button data-acl="<?=$_SESSION['acl']['ratelimit'];?>" class="btn btn-default" data-action="edit_selected" data-id="domratelimit" data-item="<?=$domain;?>" data-api-url='edit/rl-domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
-        </div>
-      </form>
-      <hr>
-      <div class="row">
-        <div class="col-sm-6">
-          <h4><?=$lang['user']['spamfilter_wl'];?></h4>
-          <p><?=$lang['user']['spamfilter_wl_desc'];?></p>
-          <div class="table-responsive">
-            <table class="table table-striped table-condensed" id="wl_policy_domain_table"></table>
-          </div>
-          <div class="mass-actions-user">
-            <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
-              <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
-              <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
-            </div>
-          </div>
-          <form class="form-inline" data-id="add_wl_policy_domain">
-            <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
-              <input type="text" class="form-control" name="object_from" placeholder="*@example.org" required>
-              <span class="input-group-btn">
-                <button class="btn btn-default" data-action="add_item" data-id="add_wl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"wl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
-              </span>
+            <?php
+            }
+            ?>
             </div>
             </div>
-          </form>
-        </div>
-        <div class="col-sm-6">
-          <h4><?=$lang['user']['spamfilter_bl'];?></h4>
-          <p><?=$lang['user']['spamfilter_bl_desc'];?></p>
-          <div class="table-responsive">
-            <table class="table table-striped table-condensed" id="bl_policy_domain_table"></table>
-          </div>
-          <div class="mass-actions-user">
-            <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
-              <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
-              <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+            <div id="dratelimit" class="tab-pane">
+              <form data-id="domratelimit" class="form-inline well" method="post">
+                <div class="form-group">
+                  <label class="control-label"><?=$lang['edit']['ratelimit'];?></label>
+                  <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" autocomplete="off" class="form-control" placeholder="disabled">
+                </div>
+                <div class="form-group">
+                  <select name="rl_frame" class="form-control">
+                    <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
+                    <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
+                    <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
+                    <option value="d" <?=(isset($rl['frame']) && $rl['frame'] == 'd') ? 'selected' : null;?>>msgs / day</option>
+                  </select>
+                </div>
+                <div class="form-group">
+                  <button data-acl="<?=$_SESSION['acl']['ratelimit'];?>" class="btn btn-default" data-action="edit_selected" data-id="domratelimit" data-item="<?=$domain;?>" data-api-url='edit/rl-domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+                </div>
+              </form>
+            </div>
+            <div id="dspamfilter" class="tab-pane">
+              <div class="row">
+                <div class="col-sm-6">
+                  <h4><?=$lang['user']['spamfilter_wl'];?></h4>
+                  <p><?=$lang['user']['spamfilter_wl_desc'];?></p>
+                  <div class="table-responsive">
+                    <table class="table table-striped table-condensed" id="wl_policy_domain_table"></table>
+                  </div>
+                  <div class="mass-actions-user">
+                    <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
+                      <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
+                      <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                    </div>
+                  </div>
+                  <form class="form-inline" data-id="add_wl_policy_domain">
+                    <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
+                      <input type="text" class="form-control" name="object_from" placeholder="*@example.org" required>
+                      <span class="input-group-btn">
+                        <button class="btn btn-default" data-action="add_item" data-id="add_wl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"wl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
+                      </span>
+                    </div>
+                  </form>
+                </div>
+                <div class="col-sm-6">
+                  <h4><?=$lang['user']['spamfilter_bl'];?></h4>
+                  <p><?=$lang['user']['spamfilter_bl_desc'];?></p>
+                  <div class="table-responsive">
+                    <table class="table table-striped table-condensed" id="bl_policy_domain_table"></table>
+                  </div>
+                  <div class="mass-actions-user">
+                    <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
+                      <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
+                      <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                    </div>
+                  </div>
+                  <form class="form-inline" data-id="add_bl_policy_domain">
+                    <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
+                      <input type="text" class="form-control" name="object_from" placeholder="*@example.org" required>
+                      <span class="input-group-btn">
+                        <button class="btn btn-default" data-action="add_item" data-id="add_bl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"bl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
+                      </span>
+                    </div>
+                  </form>
+                </div>
+              </div>
             </div>
             </div>
           </div>
           </div>
-          <form class="form-inline" data-id="add_bl_policy_domain">
-            <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
-              <input type="text" class="form-control" name="object_from" placeholder="*@example.org" required>
-              <span class="input-group-btn">
-                <button class="btn btn-default" data-action="add_item" data-id="add_bl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"bl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
-              </span>
-            </div>
-          </form>
-        </div>
-      </div>
           <?php
           <?php
         }
         }
         else {
         else {
@@ -582,6 +614,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <input type="hidden" value="0" name="force_pw_update">
           <input type="hidden" value="0" name="force_pw_update">
           <input type="hidden" value="0" name="sogo_access">
           <input type="hidden" value="0" name="sogo_access">
           <input type="hidden" value="0" name="protocol_access">
           <input type="hidden" value="0" name="protocol_access">
+          <input type="hidden" value="0" name="xmpp_access">
+          <input type="hidden" value="0" name="xmpp_admin">
           <div class="form-group">
           <div class="form-group">
             <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?></label>
             <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?></label>
             <div class="col-sm-10">
             <div class="col-sm-10">
@@ -774,6 +808,23 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             </div>
           </div>
           </div>
           <hr>
           <hr>
+          <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+              <div class="checkbox">
+                <label><input type="checkbox" data-acl="<?=$_SESSION['acl']['xmpp_mailbox_access'];?>" value="1" name="xmpp_access" <?=(isset($result['attributes']['xmpp_access']) && $result['attributes']['xmpp_access']=="1") ? "checked" : null;?>> <?=$lang['edit']['xmpp_access'];?></label>
+                <small class="help-block"><?=$lang['edit']['xmpp_access_info'];?></small>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+              <div class="checkbox">
+                <label><input data-acl="<?=$_SESSION['acl']['xmpp_admin'];?>" type="checkbox" value="1" name="xmpp_admin" <?=(isset($result['attributes']['xmpp_admin']) && $result['attributes']['xmpp_admin']=="1") ? "checked" : null;?>> <?=$lang['edit']['xmpp_admin'];?></label>
+                <small class="help-block"><?=$lang['edit']['xmpp_admin_info'];?></small>
+              </div>
+            </div>
+          </div>
+          <hr>
           <div class="form-group">
           <div class="form-group">
             <div class="col-sm-offset-2 col-sm-10">
             <div class="col-sm-offset-2 col-sm-10">
             <select name="active" class="form-control">
             <select name="active" class="form-control">

+ 395 - 396
data/web/inc/ajax/dns_diagnostics.php

@@ -9,472 +9,471 @@ define('state_optional', " <sup>2</sup>");
 
 
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"|| $_SESSION['mailcow_cc_role'] == "domainadmin")) {
 if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"|| $_SESSION['mailcow_cc_role'] == "domainadmin")) {
 
 
-$domains = mailbox('get', 'domains');
-$alias_domains = array();
-foreach($domains as $dn) {
-  $alias_domains = array_merge($alias_domains, mailbox('get', 'alias_domains', $dn));
-}
-$domains = array_merge($domains, $alias_domains);
+  $alias_domains = array();
 
 
-if (isset($_GET['domain'])) {
-  if (is_valid_domain_name($_GET['domain'])) {
-    if (in_array($_GET['domain'], $domains)) {
+  if (isset($_GET['domain'])) {
+    $domain_details = mailbox('get', 'domain_details', $_GET['domain']);
+    if ($domain_details !== false) {
       $domain = $_GET['domain'];
       $domain = $_GET['domain'];
+      $alias_domains = array_merge($alias_domains, mailbox('get', 'alias_domains', $domain));
     }
     }
     else {
     else {
       echo "No such domain in context";
       echo "No such domain in context";
       exit();
       exit();
     }
     }
   }
   }
-  else {
-    echo "Invalid domain name";
-    exit();
+
+  $ch = curl_init('http://ip4.mailcow.email');
+  curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+  curl_setopt($ch, CURLOPT_VERBOSE, false);
+  curl_setopt($ch, CURLOPT_HEADER, false);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
+  $ip = curl_exec($ch);
+  curl_close($ch);
+
+  $ch = curl_init('http://ip6.mailcow.email');
+  curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
+  curl_setopt($ch, CURLOPT_VERBOSE, false);
+  curl_setopt($ch, CURLOPT_HEADER, false);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
+  $ip6 = curl_exec($ch);
+  curl_close($ch);
+
+  $ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
+  if (!empty($ip6)) {
+    $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6);
+    $ip6_full = str_replace('::', ':0:', $ip6_full);
+    $ip6_full = str_replace('::', ':0:', $ip6_full);
+    $ptr6 = '';
+    foreach (explode(':', $ip6_full) as $part) {
+      $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT);
+    }
+    $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa';
   }
   }
-}
 
 
-$ch = curl_init('http://ip4.mailcow.email');
-curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
-curl_setopt($ch, CURLOPT_VERBOSE, false);
-curl_setopt($ch, CURLOPT_HEADER, false);
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
-$ip = curl_exec($ch);
-curl_close($ch);
-
-$ch = curl_init('http://ip6.mailcow.email');
-curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
-curl_setopt($ch, CURLOPT_VERBOSE, false);
-curl_setopt($ch, CURLOPT_HEADER, false);
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
-$ip6 = curl_exec($ch);
-curl_close($ch);
-
-$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
-if (!empty($ip6)) {
-  $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6);
-  $ip6_full = str_replace('::', ':0:', $ip6_full);
-  $ip6_full = str_replace('::', ':0:', $ip6_full);
-  $ptr6 = '';
-  foreach (explode(':', $ip6_full) as $part) {
-    $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT);
+  $https_port = strpos($_SERVER['HTTP_HOST'], ':');
+  if ($https_port === FALSE) {
+    $https_port = 443;
+  }
+  else {
+    $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1);
   }
   }
-  $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa';
-}
 
 
-$https_port = strpos($_SERVER['HTTP_HOST'], ':');
-if ($https_port === FALSE) {
-  $https_port = 443;
-}
-else {
-  $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1);
-}
+  if (!isset($autodiscover_config['sieve'])) {
+    $autodiscover_config['sieve'] = array(
+      'server' => $mailcow_hostname,
+      'port' => array_pop(explode(':', getenv('SIEVE_PORT')))
+    );
+  }
 
 
-if (!isset($autodiscover_config['sieve'])) {
-  $autodiscover_config['sieve'] = array('server' => $mailcow_hostname, 'port' => array_pop(explode(':', getenv('SIEVE_PORT'))));
-}
+  // Init records array
+  $spf_link = '<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank">SPF Record Syntax</a><br />';
+  $dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
 
 
-// Init records array
-$spf_link = '<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank">SPF Record Syntax</a><br />';
-$dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
+  $records = array();
 
 
-$records = array();
-if ($_SESSION['mailcow_cc_role'] == "admin") {
-  $records[] = array(
-    $mailcow_hostname,
-    'A',
-    $ip
-  );
-  $records[] = array(
-    $ptr,
-    'PTR',
-    $mailcow_hostname
-  );
-  if (!empty($ip6)) {
+  if ($_SESSION['mailcow_cc_role'] == "admin") {
     $records[] = array(
     $records[] = array(
       $mailcow_hostname,
       $mailcow_hostname,
-      'AAAA',
-      expand_ipv6($ip6)
+      'A',
+      $ip
     );
     );
     $records[] = array(
     $records[] = array(
-      $ptr6,
+      $ptr,
       'PTR',
       'PTR',
       $mailcow_hostname
       $mailcow_hostname
     );
     );
+    if (!empty($ip6)) {
+      $records[] = array(
+        $mailcow_hostname,
+        'AAAA',
+        expand_ipv6($ip6)
+      );
+      $records[] = array(
+        $ptr6,
+        'PTR',
+        $mailcow_hostname
+      );
+    }
+    $records[] = array(
+      '_25._tcp.' . $autodiscover_config['smtp']['server'],
+      'TLSA',
+      generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1)
+    );
   }
   }
+
   $records[] = array(
   $records[] = array(
-    '_25._tcp.'.$autodiscover_config['smtp']['server'],
-    'TLSA',
-    generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1)
+    $domain,
+    'MX',
+    $mailcow_hostname
   );
   );
+
   if (!in_array($domain, $alias_domains)) {
   if (!in_array($domain, $alias_domains)) {
     $records[] = array(
     $records[] = array(
-      '_'.$https_port.
-      '._tcp.'.$mailcow_hostname,
-      'TLSA',
-      generate_tlsa_digest($mailcow_hostname, $https_port)
-    );
-    $records[] = array(
-      '_'.$autodiscover_config['pop3']['tlsport'].
-      '._tcp.'.$autodiscover_config['pop3']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['tlsport'], 1)
-    );
-    $records[] = array(
-      '_'.$autodiscover_config['imap']['tlsport'].
-      '._tcp.'.$autodiscover_config['imap']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['tlsport'], 1)
-    );
-    $records[] = array(
-      '_'.$autodiscover_config['smtp']['port'].
-      '._tcp.'.$autodiscover_config['smtp']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['port'])
-    );
-    $records[] = array(
-      '_'.$autodiscover_config['smtp']['tlsport'].
-      '._tcp.'.$autodiscover_config['smtp']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['tlsport'], 1)
-    );
-    $records[] = array(
-      '_'.$autodiscover_config['imap']['port'].
-      '._tcp.'.$autodiscover_config['imap']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['port'])
+      'autodiscover.' . $domain,
+      'CNAME',
+      $mailcow_hostname
     );
     );
     $records[] = array(
     $records[] = array(
-      '_'.$autodiscover_config['pop3']['port'].
-      '._tcp.'.$autodiscover_config['pop3']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['port'])
+      '_autodiscover._tcp.' . $domain,
+      'SRV',
+      $mailcow_hostname . ' ' . $https_port
     );
     );
     $records[] = array(
     $records[] = array(
-      '_'.$autodiscover_config['sieve']['port'].
-      '._tcp.'.$autodiscover_config['sieve']['server'],
-      'TLSA',
-      generate_tlsa_digest($autodiscover_config['sieve']['server'], $autodiscover_config['sieve']['port'], 1)
+      'autoconfig.' . $domain,
+      'CNAME',
+      $mailcow_hostname
     );
     );
+    if ($domain_details['xmpp'] === 1 && isset($domain_details['xmpp_prefix'])) {
+      $records[] = array(
+        $domain_details['xmpp_prefix'] . '.' . $domain,
+        'CNAME',
+        $mailcow_hostname
+      );
+      $records[] = array(
+        '*.' . $domain_details['xmpp_prefix'] . '.' . $domain,
+        'CNAME',
+        $mailcow_hostname
+      );
+      $records[] = array(
+        '_xmpp-client._tcp.' . $domain_details['xmpp_prefix'] . '.' . $domain,
+        'SRV',
+        $mailcow_hostname . ' ' . array_pop(explode(':', getenv('XMPP_C22_PORT')))
+      );
+      $records[] = array(
+        '_xmpp-server._tcp.' . $domain_details['xmpp_prefix'] . '.' . $domain,
+        'SRV',
+        $mailcow_hostname . ' ' . array_pop(explode(':', getenv('XMPP_S2S_PORT')))
+      );
+    }
   }
   }
-}
-$records[] = array(
-  $domain,
-  'MX',
-  $mailcow_hostname
-);
-if (!in_array($domain, $alias_domains)) {
-  $records[] = array(
-    'autodiscover.'.$domain,
-    'CNAME',
-    $mailcow_hostname
-  );
-  $records[] = array(
-    '_autodiscover._tcp.'.$domain,
-    'SRV',
-    $mailcow_hostname.
-    ' '.$https_port
-  );
+
   $records[] = array(
   $records[] = array(
-    'autoconfig.'.$domain,
-    'CNAME',
-    $mailcow_hostname
+    $domain,
+    'TXT',
+    $spf_link,
+    state_optional
   );
   );
-}
-$records[] = array(
-  $domain,
-  'TXT',
-  $spf_link,
-  state_optional
-);
-$records[] = array(
-  '_dmarc.'.$domain,
-  'TXT',
-  $dmarc_link,
-  state_optional
-);
-
-if (!empty($dkim = dkim('details', $domain))) {
+
   $records[] = array(
   $records[] = array(
-    $dkim['dkim_selector'] . '._domainkey.' . $domain,
+    '_dmarc.' . $domain,
     'TXT',
     'TXT',
-    $dkim['dkim_txt']
+    $dmarc_link,
+    state_optional
   );
   );
-}
-if (!in_array($domain, $alias_domains)) {
-  $current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV);
-  if (count($current_records) == 0 || $current_records[0]['target'] != '') {
-    if ($autodiscover_config['pop3']['tlsport'] != '110') {
+
+  if (!empty($dkim = dkim('details', $domain))) {
+    $records[] = array(
+      $dkim['dkim_selector'] . '._domainkey.' . $domain,
+      'TXT',
+      $dkim['dkim_txt']
+    );
+  }
+
+  if (!in_array($domain, $alias_domains)) {
+    $current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV);
+    if (count($current_records) == 0 || $current_records[0]['target'] != '') {
+      if ($autodiscover_config['pop3']['tlsport'] != '110') {
+        $records[] = array(
+          '_pop3._tcp.' . $domain,
+          'SRV',
+          $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']
+        );
+      }
+    }
+    else {
       $records[] = array(
       $records[] = array(
         '_pop3._tcp.' . $domain,
         '_pop3._tcp.' . $domain,
         'SRV',
         'SRV',
-        $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']
+        '. 0'
       );
       );
     }
     }
-  }
-  else {
-    $records[] = array(
-      '_pop3._tcp.' . $domain,
-      'SRV',
-      '. 0'
-    );
-  }
-  $current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV);
-  if (count($current_records) == 0 || $current_records[0]['target'] != '') {
-    if ($autodiscover_config['pop3']['port'] != '995') {
+
+    $current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV);
+
+    if (count($current_records) == 0 || $current_records[0]['target'] != '') {
+      if ($autodiscover_config['pop3']['port'] != '995') {
+        $records[] = array(
+          '_pop3s._tcp.' . $domain,
+          'SRV',
+          $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']
+        );
+      }
+    }
+    else {
       $records[] = array(
       $records[] = array(
         '_pop3s._tcp.' . $domain,
         '_pop3s._tcp.' . $domain,
         'SRV',
         'SRV',
-        $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']
+        '. 0'
       );
       );
     }
     }
-  }
-  else {
-    $records[] = array(
-      '_pop3s._tcp.' . $domain,
-      'SRV',
-      '. 0'
-    );
-  }
-  if ($autodiscover_config['imap']['tlsport'] != '143') {
-    $records[] = array(
-      '_imap._tcp.' . $domain,
-      'SRV',
-      $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']
-    );
-  }
-  if ($autodiscover_config['imap']['port'] != '993') {
-    $records[] = array(
-      '_imaps._tcp.' . $domain,
-      'SRV',
-      $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']
-    );
-  }
-  if ($autodiscover_config['smtp']['tlsport'] != '587') {
-    $records[] = array(
-      '_submission._tcp.' . $domain,
-      'SRV',
-      $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']
-    );
-  }
-  if ($autodiscover_config['smtp']['port'] != '465') {
-    $records[] = array(
-      '_smtps._tcp.' . $domain,
-      'SRV',
-      $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']
-    );
-  }
-  if ($autodiscover_config['sieve']['port'] != '4190') {
-    $records[] = array(
-      '_sieve._tcp.' . $domain,
-      'SRV',
-      $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']
-    );
-  }
-}
-
-$record_types = array(
-  'A' => DNS_A,
-  'AAAA' => DNS_AAAA,
-  'CNAME' => DNS_CNAME,
-  'MX' => DNS_MX,
-  'PTR' => DNS_PTR,
-  'SRV' => DNS_SRV,
-  'TXT' => DNS_TXT,
-);
-$data_field = array(
-  'A' => 'ip',
-  'AAAA' => 'ipv6',
-  'CNAME' => 'target',
-  'MX' => 'target',
-  'PTR' => 'target',
-  'SRV' => 'data',
-  'TLSA' => 'data',
-  'TXT' => 'txt',
-);
 
 
-?>
-<div class="table-responsive" id="dnstable">
-  <table class="table table-striped">
-    <tr>
-      <th><?=$lang['diagnostics']['dns_records_name'];?></th>
-      <th><?=$lang['diagnostics']['dns_records_type'];?></th>
-      <th><?=$lang['diagnostics']['dns_records_data'];?></th>
-      <th><?=$lang['diagnostics']['dns_records_status'];?></th>
-    </tr>
-<?php
-foreach ($records as &$record) {
-  $record[1] = strtoupper($record[1]);
-  $state = state_missing;
-  if ($record[1] == 'TLSA') {
-    $currents = dns_get_record($record[0], 52, $_, $_, TRUE);
-    foreach ($currents as &$current) {
-      $current['type'] = 'TLSA';
-      $current['cert_usage'] = hexdec(bin2hex($current['data']{0}));
-      $current['selector'] = hexdec(bin2hex($current['data']{1}));
-      $current['match_type'] = hexdec(bin2hex($current['data']{2}));
-      $current['cert_data'] = bin2hex(substr($current['data'], 3));
-      $current['data'] = $current['cert_usage'] . ' ' . $current['selector'] . ' ' . $current['match_type'] . ' ' . $current['cert_data'];
+    if ($autodiscover_config['imap']['tlsport'] != '143') {
+      $records[] = array(
+        '_imap._tcp.' . $domain,
+        'SRV',
+        $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']
+      );
     }
     }
-    unset($current);
-  }
-  else {
-    $currents = dns_get_record($record[0], $record_types[$record[1]]);
-    if ($record[0] == $mailcow_hostname && ($record[1] == "A" || $record[1] == "AAAA")) {
-      if (!empty(dns_get_record($record[0], DNS_CNAME))) {
-        $currents[0]['ip'] = state_missing . ' <b>(CNAME)</b>';
-        $currents[0]['ipv6'] = state_missing . ' <b>(CNAME)</b>';
-      }
+
+    if ($autodiscover_config['imap']['port'] != '993') {
+      $records[] = array(
+        '_imaps._tcp.' . $domain,
+        'SRV',
+        $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']
+      );
     }
     }
-    if ($record[1] == 'SRV') {
-      foreach ($currents as &$current) {
-        if ($current['target'] == '') {
-          $current['target'] = '.';
-          $current['port'] = '0';
-        }
-        $current['data'] = $current['target'] . ' ' . $current['port'];
-      }
-      unset($current);
+
+    if ($autodiscover_config['smtp']['tlsport'] != '587') {
+      $records[] = array(
+        '_submission._tcp.' . $domain,
+        'SRV',
+        $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']
+      );
     }
     }
-    elseif ($record[1] == 'TXT') {
-      foreach ($currents as &$current) {
-        unset($current);
-      }
-      unset($current);
+
+    if ($autodiscover_config['smtp']['port'] != '465') {
+      $records[] = array(
+        '_smtps._tcp.' . $domain,
+        'SRV',
+        $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']
+      );
     }
     }
-    elseif ($record[1] == 'AAAA') {
-      foreach ($currents as &$current) {
-        $current['ipv6'] = expand_ipv6($current['ipv6']);
-      }
+
+    if ($autodiscover_config['sieve']['port'] != '4190') {
+      $records[] = array(
+        '_sieve._tcp.' . $domain,
+        'SRV',
+        $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']
+      );
     }
     }
   }
   }
 
 
-  if ($record[1] == 'CNAME' && count($currents) == 0) {
-    // A and AAAA are also valid instead of CNAME
-    $a = dns_get_record($record[0], DNS_A);
-    $cname = dns_get_record($record[2], DNS_A);
-    if (count($a) > 0 && count($cname) > 0) {
-      if ($a[0]['ip'] == $cname[0]['ip']) {
-        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $record[2]));
-        $aaaa = dns_get_record($record[0], DNS_AAAA);
-        $cname = dns_get_record($record[2], DNS_AAAA);
-        if (count($aaaa) == 0 || count($cname) == 0 || expand_ipv6($aaaa[0]['ipv6']) != expand_ipv6($cname[0]['ipv6'])) {
-          $currents[0]['target'] = expand_ipv6($aaaa[0]['ipv6']) . ' <sup>1</sup>';
+  $record_types = array(
+    'A' => DNS_A,
+    'AAAA' => DNS_AAAA,
+    'CNAME' => DNS_CNAME,
+    'MX' => DNS_MX,
+    'PTR' => DNS_PTR,
+    'SRV' => DNS_SRV,
+    'TXT' => DNS_TXT,
+  );
+
+  $data_field = array(
+    'A' => 'ip',
+    'AAAA' => 'ipv6',
+    'CNAME' => 'target',
+    'MX' => 'target',
+    'PTR' => 'target',
+    'SRV' => 'data',
+    'TLSA' => 'data',
+    'TXT' => 'txt',
+  );
+
+  ?>
+  <div class="table-responsive" id="dnstable">
+    <table class="table table-striped">
+      <tr>
+        <th><?=$lang['diagnostics']['dns_records_name'];?></th>
+        <th><?=$lang['diagnostics']['dns_records_type'];?></th>
+        <th><?=$lang['diagnostics']['dns_records_data'];?></th>
+        <th><?=$lang['diagnostics']['dns_records_status'];?></th>
+      </tr>
+      <?php
+      foreach ($records as &$record) {
+        $record[1] = strtoupper($record[1]);
+        $state = state_optional;
+
+        if ($record[1] == 'TLSA') {
+          $currents = dns_get_record($record[0], 52, $_, $_, TRUE);
+          foreach ($currents as &$current) {
+            $current['type'] = 'TLSA';
+            $current['cert_usage'] = hexdec(bin2hex($current['data']{0}));
+            $current['selector'] = hexdec(bin2hex($current['data']{1}));
+            $current['match_type'] = hexdec(bin2hex($current['data']{2}));
+            $current['cert_data'] = bin2hex(substr($current['data'], 3));
+            $current['data'] = $current['cert_usage'] . ' ' . $current['selector'] . ' ' . $current['match_type'] . ' ' . $current['cert_data'];
+          }
+          unset($current);
+        }
+        else {
+          $currents = dns_get_record($record[0], $record_types[$record[1]]);
+          if ($record[0] == $mailcow_hostname && ($record[1] == "A" || $record[1] == "AAAA")) {
+            if (!empty(dns_get_record($record[0], DNS_CNAME))) {
+              $currents[0]['ip'] = state_missing . ' <b>(CNAME)</b>';
+              $currents[0]['ipv6'] = state_missing . ' <b>(CNAME)</b>';
+            }
+          }
+          if ($record[1] == 'SRV') {
+            foreach ($currents as &$current) {
+              if ($current['target'] == '') {
+                $current['target'] = '.';
+                $current['port'] = '0';
+              }
+              $current['data'] = $current['target'] . ' ' . $current['port'];
+            }
+            unset($current);
+          }
+          elseif ($record[1] == 'TXT') {
+            foreach ($currents as &$current) {
+              unset($current);
+            }
+            unset($current);
+          }
+          elseif ($record[1] == 'AAAA') {
+            foreach ($currents as &$current) {
+              $current['ipv6'] = expand_ipv6($current['ipv6']);
+            }
+          }
         }
         }
-      }
-      else {
-        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $a[0]['ip'] . ' <sup>1</sup>'));
-      }
-    }
-  }
 
 
-  foreach ($currents as &$current) {
-    if ($current['type'] == 'TXT' &&
-      stripos($current['txt'], 'v=dmarc') === 0 &&
-      $record[2] == $dmarc_link) {
-        $current['txt'] = str_replace(' ', '', $current['txt']);
-        $state = $current[$data_field[$current['type']]] . state_optional;
-    }
-    elseif ($current['type'] == 'TXT' &&
-      stripos($current['txt'], 'v=spf') === 0 &&
-      $record[2] == $spf_link) {
-        $state = state_nomatch;
-        $rslt = get_spf_allowed_hosts($record[0]);
-        if(in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)){
-            $state = state_good;
+        if ($record[1] == 'CNAME' && count($currents) == 0) {
+          // A and AAAA are also valid instead of CNAME
+          $a = dns_get_record($record[0], DNS_A);
+          $cname = dns_get_record($record[2], DNS_A);
+          if (count($a) > 0 && count($cname) > 0) {
+            if ($a[0]['ip'] == $cname[0]['ip']) {
+              $currents = array(
+                array(
+                  'host' => $record[0],
+                  'class' => 'IN',
+                  'type' => 'CNAME',
+                  'target' => $record[2]
+                )
+              );
+              $aaaa = dns_get_record($record[0], DNS_AAAA);
+              $cname = dns_get_record($record[2], DNS_AAAA);
+              if (count($aaaa) == 0 || count($cname) == 0 || expand_ipv6($aaaa[0]['ipv6']) != expand_ipv6($cname[0]['ipv6'])) {
+                $currents[0]['target'] = expand_ipv6($aaaa[0]['ipv6']) . ' <sup>1</sup>';
+              }
+            }
+            else {
+              $currents = array(
+                array(
+                  'host' => $record[0],
+                  'class' => 'IN',
+                  'type' => 'CNAME',
+                  'target' => $a[0]['ip'] . ' <sup>1</sup>'
+                )
+              );
+            }
+          }
         }
         }
-        $state .= '<br />' . $current[$data_field[$current['type']]].state_optional;
-    }
-    elseif ($current['type'] == 'TXT' &&
-      stripos($current['txt'], 'v=dkim') === 0 &&
-      stripos($record[2], 'v=dkim') === 0) {
-        preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
-        preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
-        if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
-          $state = state_good;
+
+        foreach ($currents as &$current) {
+          if ($current['type'] == 'TXT' &&
+          stripos($current['txt'], 'v=dmarc') === 0 &&
+          $record[2] == $dmarc_link) {
+            $current['txt'] = str_replace(' ', '', $current['txt']);
+            $state = $current[$data_field[$current['type']]] . state_optional;
+          }
+          elseif ($current['type'] == 'TXT' &&
+          stripos($current['txt'], 'v=spf') === 0 &&
+          $record[2] == $spf_link) {
+            $state = state_nomatch;
+            $rslt = get_spf_allowed_hosts($record[0], true);
+            if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
+              $state = state_good;
+            }
+            $state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
+          }
+          elseif ($current['type'] == 'TXT' &&
+          stripos($current['txt'], 'v=dkim') === 0 &&
+          stripos($record[2], 'v=dkim') === 0) {
+            preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
+            preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
+            if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
+              $state = state_good;
+            }
+          }
+          elseif ($current['type'] != 'TXT' &&
+          isset($data_field[$current['type']]) && $state != state_good) {
+            $state = state_nomatch;
+            if ($current[$data_field[$current['type']]] == $record[2]) {
+              $state = state_good;
+            }
+          }
         }
         }
-    }
-    elseif ($current['type'] != 'TXT' &&
-      isset($data_field[$current['type']]) && $state != state_good) {
-        $state = state_nomatch;
-        if ($current[$data_field[$current['type']]] == $record[2]) {
-          $state = state_good;
+        unset($current);
+
+        if (isset($record[3]) &&
+        $record[3] == state_optional &&
+        ($state == state_missing || $state == state_nomatch)) {
+          $state = state_optional;
         }
         }
-    }
-  }
-  unset($current);
 
 
-  if (isset($record[3]) &&
-    $record[3] == state_optional &&
-    ($state == state_missing || $state == state_nomatch)) {
-      $state = state_optional;
-  }
-  
-  if ($state == state_nomatch) {
-    $state = array();
-    foreach ($currents as $current) {
-      $state[] = $current[$data_field[$current['type']]];
-    }
-    $state = implode('<br />', $state);
-  }
-  echo sprintf('<tr>
-    <td>%s</td>
-    <td>%s</td>
-    <td class="dns-found">%s</td>
-    <td class="dns-recommended">%s</td>
-  </tr>', $record[0], $record[1], $record[2], $state);
-  $record[3] = explode('<br />', $state);
-}
-unset($record);
-
-$dns_data = sprintf("\$ORIGIN %s.\n", $domain);
-foreach ($records as $record) {
-  if ($domain == substr($record[0], -strlen($domain))) {
-    $label = substr($record[0], 0, -strlen($domain)-1);
-    $val = $record[2];
-    if (strlen($label) == 0) {
-      $label = "@";
-    }
-    $vals = array();
-    if (strpos($val, "<a") !== FALSE) {
-      if(is_array($record[3]) && count($record[3]) == 1 && $record[3][0] == state_optional) {
-        $record[3][0] = "**TODO**";
-        $label = ';' . $label;
-      }
-      foreach ($record[3] as $val) {
-        $val = str_replace(state_optional, '', $val);
-        $val = str_replace(state_good, '', $val);
-        if (strlen($val) > 0) {
-          $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
+        if ($state == state_nomatch) {
+          $state = array();
+          foreach ($currents as $current) {
+            $state[] = $current[$data_field[$current['type']]];
+          }
+          $state = implode('<br />', $state);
         }
         }
+        echo sprintf('
+        <tr>
+          <td>%s</td>
+          <td>%s</td>
+          <td class="dns-found">%s</td>
+          <td class="dns-recommended">%s</td>
+        </tr>', $record[0], $record[1], $record[2], $state);
+        $record[3] = explode('<br />', $state);
       }
       }
-    }
-    else {
-      $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
-    }
-    foreach ($vals as $val) {
-      $dns_data .= str_replace($domain, $domain . '.', $val);
-    }
-  }
-}
 
 
-?>
-  </table>
-  <a id='download-zonefile' data-zonefile="<?=base64_encode($dns_data);?>" download='<?=$_GET['domain'];?>.txt' type='text/csv'>Download</a>
-  <script>
+      unset($record);
+
+      $dns_data = sprintf("\$ORIGIN %s.\n", $domain);
+      foreach ($records as $record) {
+        if ($domain == substr($record[0], -strlen($domain))) {
+          $label = substr($record[0], 0, -strlen($domain)-1);
+          $val = $record[2];
+
+          if (strlen($label) == 0) {
+            $label = "@";
+          }
+
+          $vals = array();
+          if (strpos($val, "<a") !== FALSE) {
+            if (is_array($record[3]) && count($record[3]) == 1 && $record[3][0] == state_optional) {
+              $record[3][0] = "**TODO**";
+              $label = ';' . $label;
+            }
+            foreach ($record[3] as $val) {
+              $val = str_replace(state_optional, '', $val);
+              $val = str_replace(state_good, '', $val);
+              if (strlen($val) > 0) {
+                $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
+              }
+            }
+          }
+          else {
+            $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
+          }
+
+          foreach ($vals as $val) {
+            $dns_data .= str_replace($domain, $domain . '.', $val);
+          }
+        }
+      }
+      ?>
+    </table>
+    <a id='download-zonefile' data-zonefile="<?=base64_encode($dns_data);?>" download='<?=$_GET['domain'];?>.txt' type='text/csv'>Download</a>
+    <script>
       var zonefile_dl_link = document.getElementById('download-zonefile');
       var zonefile_dl_link = document.getElementById('download-zonefile');
       var zonefile = atob(zonefile_dl_link.getAttribute('data-zonefile'));
       var zonefile = atob(zonefile_dl_link.getAttribute('data-zonefile'));
       var data = new Blob([zonefile]);
       var data = new Blob([zonefile]);
       var download_zonefile_link = document.getElementById('download-zonefile');
       var download_zonefile_link = document.getElementById('download-zonefile');
       download_zonefile_link.href = URL.createObjectURL(data);
       download_zonefile_link.href = URL.createObjectURL(data);
-  </script>
-</div>
-<p class="help-block">
-<sup>1</sup> <?=$lang['diagnostics']['cname_from_a'];?><br />
-<sup>2</sup> <?=$lang['diagnostics']['optional'];?>
-</p>
-<?php
-} else {
+    </script>
+  </div>
+  <p class="help-block">
+    <sup>1</sup> <?=$lang['diagnostics']['cname_from_a'];?><br />
+    <sup>2</sup> <?=$lang['diagnostics']['optional'];?>
+  </p>
+  <?php
+}
+else {
   echo "Session invalid";
   echo "Session invalid";
   exit();
   exit();
 }
 }

+ 40 - 3
data/web/inc/functions.mailbox.inc.php

@@ -443,6 +443,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           }
           $domain				= idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $domain				= idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $description  = $_data['description'];
           $description  = $_data['description'];
+          $xmpp_prefix  = $_data['xmpp_prefix'];
           if (empty($description)) {
           if (empty($description)) {
             $description = $domain;
             $description = $domain;
           }
           }
@@ -489,6 +490,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $relay_unknown_only = intval($_data['relay_unknown_only']);
           $relay_unknown_only = intval($_data['relay_unknown_only']);
           $backupmx = intval($_data['backupmx']);
           $backupmx = intval($_data['backupmx']);
           $gal = intval($_data['gal']);
           $gal = intval($_data['gal']);
+          $xmpp = intval($_data['xmpp']);
           if ($relay_all_recipients == 1) {
           if ($relay_all_recipients == 1) {
             $backupmx = '1';
             $backupmx = '1';
           }
           }
@@ -542,8 +544,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $stmt->execute(array(
           $stmt->execute(array(
             ':domain' => '%@' . $domain
             ':domain' => '%@' . $domain
           ));
           ));
-          $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`)
-            VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients)");
+          $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `xmpp`, `xmpp_prefix`, `active`, `relay_unknown_only`, `relay_all_recipients`)
+            VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :xmpp, :xmpp_prefix, :active, :relay_unknown_only, :relay_all_recipients)");
           $stmt->execute(array(
           $stmt->execute(array(
             ':domain' => $domain,
             ':domain' => $domain,
             ':description' => $description,
             ':description' => $description,
@@ -554,6 +556,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':quota' => $quota,
             ':quota' => $quota,
             ':backupmx' => $backupmx,
             ':backupmx' => $backupmx,
             ':gal' => $gal,
             ':gal' => $gal,
+            ':xmpp' => $xmpp,
+            ':xmpp_prefix' => $xmpp_prefix,
             ':active' => $active,
             ':active' => $active,
             ':relay_unknown_only' => $relay_unknown_only,
             ':relay_unknown_only' => $relay_unknown_only,
             ':relay_all_recipients' => $relay_all_recipients
             ':relay_all_recipients' => $relay_all_recipients
@@ -948,6 +952,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $imap_access = (isset($_data['imap_access'])) ? intval($_data['imap_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
           $imap_access = (isset($_data['imap_access'])) ? intval($_data['imap_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
           $pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
           $pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
           $smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
           $smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
+          $xmpp_access = (isset($_data['xmpp_access'])) ? intval($_data['xmpp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['xmpp_access']);
+          $xmpp_admin = (isset($_data['xmpp_admin'])) ? intval($_data['xmpp_admin']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['xmpp_admin']);
           $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
           $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
           $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
           $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
           $quota_b		= ($quota_m * 1048576);
           $quota_b		= ($quota_m * 1048576);
@@ -960,6 +966,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               'imap_access' => strval($imap_access),
               'imap_access' => strval($imap_access),
               'pop3_access' => strval($pop3_access),
               'pop3_access' => strval($pop3_access),
               'smtp_access' => strval($smtp_access),
               'smtp_access' => strval($smtp_access),
+              'xmpp_access' => strval($xmpp_access),
+              'xmpp_admin' => strval($xmpp_admin),
               'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
               'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
               'quarantine_notification' => strval($quarantine_notification),
               'quarantine_notification' => strval($quarantine_notification),
               'quarantine_category' => strval($quarantine_category)
               'quarantine_category' => strval($quarantine_category)
@@ -2095,6 +2103,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $is_now = mailbox('get', 'domain_details', $domain);
               $is_now = mailbox('get', 'domain_details', $domain);
               if (!empty($is_now)) {
               if (!empty($is_now)) {
                 $gal                  = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
                 $gal                  = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
+                $xmpp                 = (isset($_data['xmpp']) && !empty($_SESSION['acl']['xmpp_domain_access']) && $_SESSION['acl']['xmpp_domain_access'] == "1") ? intval($_data['xmpp']) : $is_now['xmpp'];
+                $xmpp_prefix          = (!empty($_data['xmpp_prefix']) && !empty($_SESSION['acl']['xmpp_prefix']) && $_SESSION['acl']['xmpp_prefix'] == "1") ? $_data['xmpp_prefix'] : $is_now['xmpp_prefix'];
                 $description          = (!empty($_data['description']) && isset($_SESSION['acl']['domain_desc']) && $_SESSION['acl']['domain_desc'] == "1") ? $_data['description'] : $is_now['description'];
                 $description          = (!empty($_data['description']) && isset($_SESSION['acl']['domain_desc']) && $_SESSION['acl']['domain_desc'] == "1") ? $_data['description'] : $is_now['description'];
               }
               }
               else {
               else {
@@ -2107,11 +2117,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               }
               }
               $stmt = $pdo->prepare("UPDATE `domain` SET
               $stmt = $pdo->prepare("UPDATE `domain` SET
               `description` = :description,
               `description` = :description,
-              `gal` = :gal
+              `gal` = :gal,
+              `xmpp` = :xmpp,
+              `xmpp_prefix` = :xmpp_prefix
                 WHERE `domain` = :domain");
                 WHERE `domain` = :domain");
               $stmt->execute(array(
               $stmt->execute(array(
                 ':description' => $description,
                 ':description' => $description,
                 ':gal' => $gal,
                 ':gal' => $gal,
+                ':xmpp' => $xmpp,
+                ':xmpp_prefix' => $xmpp_prefix,
                 ':domain' => $domain
                 ':domain' => $domain
               ));
               ));
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
@@ -2126,6 +2140,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $active               = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
                 $active               = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
                 $backupmx             = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $is_now['backupmx'];
                 $backupmx             = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $is_now['backupmx'];
                 $gal                  = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
                 $gal                  = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
+                $xmpp                 = (isset($_data['xmpp'])) ? intval($_data['xmpp']) : $is_now['xmpp'];
                 $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $is_now['relay_all_recipients'];
                 $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $is_now['relay_all_recipients'];
                 $relay_unknown_only   = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $is_now['relay_unknown_only'];
                 $relay_unknown_only   = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $is_now['relay_unknown_only'];
                 $relayhost            = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost'];
                 $relayhost            = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost'];
@@ -2135,6 +2150,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $maxquota             = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
                 $maxquota             = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
                 $quota                = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
                 $quota                = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
                 $description          = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
                 $description          = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
+                $xmpp_prefix          = (!empty($_data['xmpp_prefix'])) ? $_data['xmpp_prefix'] : $is_now['xmpp_prefix'];
                 if ($relay_all_recipients == '1') {
                 if ($relay_all_recipients == '1') {
                   $backupmx = '1';
                   $backupmx = '1';
                 }
                 }
@@ -2238,6 +2254,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `relay_unknown_only` = :relay_unknown_only,
               `relay_unknown_only` = :relay_unknown_only,
               `backupmx` = :backupmx,
               `backupmx` = :backupmx,
               `gal` = :gal,
               `gal` = :gal,
+              `xmpp` = :xmpp,
+              `xmpp_prefix` = :xmpp_prefix,
               `active` = :active,
               `active` = :active,
               `quota` = :quota,
               `quota` = :quota,
               `defquota` = :defquota,
               `defquota` = :defquota,
@@ -2252,6 +2270,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ':relay_unknown_only' => $relay_unknown_only,
                 ':relay_unknown_only' => $relay_unknown_only,
                 ':backupmx' => $backupmx,
                 ':backupmx' => $backupmx,
                 ':gal' => $gal,
                 ':gal' => $gal,
+                ':xmpp' => $xmpp,
+                ':xmpp_prefix' => $xmpp_prefix,
                 ':active' => $active,
                 ':active' => $active,
                 ':quota' => $quota,
                 ':quota' => $quota,
                 ':defquota' => $defquota,
                 ':defquota' => $defquota,
@@ -2300,6 +2320,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
               (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
               (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
               (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
               (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
               (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
+              (int)$xmpp_admin = (isset($_data['xmpp_admin']) && isset($_SESSION['acl']['xmpp_admin']) && $_SESSION['acl']['xmpp_admin'] == "1") ? intval($_data['xmpp_admin']) : intval($is_now['attributes']['xmpp_admin']);
+              (int)$xmpp_access = (isset($_data['xmpp_access']) && isset($_SESSION['acl']['xmpp_mailbox_access']) && $_SESSION['acl']['xmpp_mailbox_access'] == "1") ? intval($_data['xmpp_access']) : intval($is_now['attributes']['xmpp_access']);
               (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
               (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
               $name       = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
               $name       = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
               $domain     = $is_now['domain'];
               $domain     = $is_now['domain'];
@@ -2587,6 +2609,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
                 `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
                 `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
                 `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
                 `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
                 `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
+                `attributes` = JSON_SET(`attributes`, '$.xmpp_admin', :xmpp_admin),
+                `attributes` = JSON_SET(`attributes`, '$.xmpp_access', :xmpp_access),
                 `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access)
                 `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access)
                   WHERE `username` = :username");
                   WHERE `username` = :username");
             $stmt->execute(array(
             $stmt->execute(array(
@@ -2598,6 +2622,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               ':imap_access' => $imap_access,
               ':imap_access' => $imap_access,
               ':pop3_access' => $pop3_access,
               ':pop3_access' => $pop3_access,
               ':smtp_access' => $smtp_access,
               ':smtp_access' => $smtp_access,
+              ':xmpp_admin' => $xmpp_admin,
+              ':xmpp_access' => $xmpp_access,
               ':username' => $username
               ':username' => $username
             ));
             ));
             $_SESSION['return'][] = array(
             $_SESSION['return'][] = array(
@@ -3353,6 +3379,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `relay_unknown_only`,
               `relay_unknown_only`,
               `backupmx`,
               `backupmx`,
               `gal`,
               `gal`,
+              `xmpp`,
+              `xmpp_prefix`,
               `active`
               `active`
                 FROM `domain` WHERE `domain`= :domain");
                 FROM `domain` WHERE `domain`= :domain");
           $stmt->execute(array(
           $stmt->execute(array(
@@ -3411,6 +3439,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $domaindata['backupmx'] = $row['backupmx'];
           $domaindata['backupmx'] = $row['backupmx'];
           $domaindata['backupmx_int'] = $row['backupmx'];
           $domaindata['backupmx_int'] = $row['backupmx'];
           $domaindata['gal'] = $row['gal'];
           $domaindata['gal'] = $row['gal'];
+          $domaindata['xmpp'] = $row['xmpp'];
+          $domaindata['xmpp_prefix'] = $row['xmpp_prefix'];
           $domaindata['gal_int'] = $row['gal'];
           $domaindata['gal_int'] = $row['gal'];
           $domaindata['rl'] = $rl;
           $domaindata['rl'] = $rl;
           $domaindata['active'] = $row['active'];
           $domaindata['active'] = $row['active'];
@@ -3469,6 +3499,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`domain`,
               `mailbox`.`domain`,
               `mailbox`.`local_part`,
               `mailbox`.`local_part`,
               `mailbox`.`quota`,
               `mailbox`.`quota`,
+              `domain`.`xmpp` AS `domain_xmpp`,
+              `domain`.`xmpp_prefix` AS `domain_xmpp_prefix`,
               `quota2`.`bytes`,
               `quota2`.`bytes`,
               `attributes`,
               `attributes`,
               `quota2`.`messages`
               `quota2`.`messages`
@@ -3484,6 +3516,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`domain`,
               `mailbox`.`domain`,
               `mailbox`.`local_part`,
               `mailbox`.`local_part`,
               `mailbox`.`quota`,
               `mailbox`.`quota`,
+              `domain`.`xmpp` AS `domain_xmpp`,
+              `domain`.`xmpp_prefix` AS `domain_xmpp_prefix`,
               `quota2replica`.`bytes`,
               `quota2replica`.`bytes`,
               `attributes`,
               `attributes`,
               `quota2replica`.`messages`
               `quota2replica`.`messages`
@@ -3527,6 +3561,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['active'] = $row['active'];
           $mailboxdata['active'] = $row['active'];
           $mailboxdata['active_int'] = $row['active'];
           $mailboxdata['active_int'] = $row['active'];
           $mailboxdata['domain'] = $row['domain'];
           $mailboxdata['domain'] = $row['domain'];
+          $mailboxdata['domain_xmpp'] = $row['domain_xmpp'];
+          $mailboxdata['domain_xmpp_prefix'] = $row['domain_xmpp_prefix'];
           $mailboxdata['local_part'] = $row['local_part'];
           $mailboxdata['local_part'] = $row['local_part'];
           $mailboxdata['quota'] = $row['quota'];
           $mailboxdata['quota'] = $row['quota'];
           $mailboxdata['attributes'] = json_decode($row['attributes'], true);
           $mailboxdata['attributes'] = json_decode($row['attributes'], true);
@@ -4268,5 +4304,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
   }
   }
   if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
   if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
     update_sogo_static_view();
     update_sogo_static_view();
+    xmpp_rebuild_configs();
   }
   }
 }
 }

+ 208 - 0
data/web/inc/functions.xmpp.inc.php

@@ -0,0 +1,208 @@
+<?php
+function xmpp_control($_action, $_data = null) {
+	global $lang;
+  $_data_log = $_data;
+  switch ($_action) {
+    case 'reload':
+      $curl = curl_init();
+      curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+      curl_setopt($curl, CURLOPT_URL, 'https://ejabberd:5443/api/reload_config');
+      curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+      curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
+      curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+      curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+      $response = curl_exec($curl);
+      curl_close($curl);
+
+      if ($response === "0") {
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => 'xmpp_reloaded'
+        );
+      }
+      else {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => 'xmpp_reload_failed'
+        );
+      }
+    break;
+    case 'restart':
+      $curl = curl_init();
+      curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+      curl_setopt($curl, CURLOPT_URL, 'https://ejabberd:5443/api/restart');
+      curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+      curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
+      curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+      curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+      $response = curl_exec($curl);
+      curl_close($curl);
+
+      if ($response === "0") {
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => 'xmpp_restarted'
+        );
+      }
+      else {
+        // If no host is available, the container might be in sleeping state, we need to restart the container
+        $response = json_decode(docker('post', 'ejabberd-mailcow', 'restart'), true);
+        if (isset($response['type']) && $response['type'] == "success") {
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => 'xmpp_restarted'
+          );
+        }
+        else {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data_log),
+            'msg' => 'xmpp_restart_failed'
+          );
+        }
+      }
+    break;
+    case 'status':
+      if ($_SESSION['mailcow_cc_role'] != "admin") {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => 'access_denied'
+        );
+        return false;
+      }
+      foreach (array(
+          'onlineusers' => 'stats?name=onlineusers',
+          'uptimeseconds' => 'stats?name=uptimeseconds',
+          'muc_online_rooms' => 'muc_online_rooms?service=global'
+        ) as $stat => $url) {
+        $curl = curl_init();
+        curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+        curl_setopt($curl, CURLOPT_URL, 'https://ejabberd:5443/api/' . $url);
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+        $response_json = json_decode(curl_exec($curl), true);
+        if (isset($response_json['stat'])) {
+          $response_data[$stat] = $response_json['stat'];
+        }
+        else {
+          $response_data[$stat] = $response_json;
+        }
+        curl_close($curl);
+        // Something went wrong
+        if ($response_data[$stat] === false) {
+          $response_data[$stat] = '?';
+        }
+      }
+      return $response_data;
+    break;
+  }
+}
+function xmpp_rebuild_configs() {
+	global $pdo;
+	global $lang;
+  $_data_log = $_data;
+
+  try {
+    $xmpp_domains = array();
+    $stmt = $pdo->query('SELECT CONCAT(`xmpp_prefix`, ".", `domain`) AS `xmpp_host`, `domain` FROM `domain` WHERE `xmpp` = 1');
+    $xmpp_domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+    foreach ($xmpp_domain_rows as $xmpp_domain_row) {
+      $xmpp_domains[$xmpp_domain_row['domain']] = array('xmpp_host' => $xmpp_domain_row['xmpp_host']);
+      $stmt = $pdo->query('SELECT CONCAT(`local_part`, "@", CONCAT(`domain`.`xmpp_prefix`, ".", `domain`.`domain`)) AS `xmpp_username` FROM `mailbox`
+        JOIN `domain`
+          WHERE `domain`.`xmpp` = 1
+            AND JSON_VALUE(`attributes`, "$.xmpp_admin") = 1');
+      $xmpp_admin_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      foreach ($xmpp_admin_rows as $xmpp_admin_row) {
+        $xmpp_domains[$xmpp_domain_row['domain']]['xmpp_admins'][] = $xmpp_admin_row['xmpp_username'];
+      }
+    }
+
+    touch('/ejabberd/ejabberd_hosts.yml');
+    touch('/ejabberd/ejabberd_acl.yml');
+    $ejabberd_hosts_md5 = md5_file('/ejabberd/ejabberd_hosts.yml');
+    $ejabberd_acl_md5 = md5_file('/ejabberd/ejabberd_acl.yml');
+
+    if (!empty($xmpp_domains)) {
+      // Handle hosts file
+      $map_handle = fopen('/ejabberd/ejabberd_hosts.yml', 'w');
+      if (!$map_handle) {
+        throw new Exception($lang['danger']['file_open_error']);
+      }
+      fwrite($map_handle, '# Autogenerated by mailcow' . PHP_EOL);
+      fwrite($map_handle, 'hosts:' . PHP_EOL);
+      foreach ($xmpp_domains as $domain => $domain_values) {
+        fwrite($map_handle, '  - ' . $xmpp_domains[$domain]['xmpp_host'] . PHP_EOL);
+      }
+      fclose($map_handle);
+
+      // Handle ACL file
+      $map_handle = fopen('/ejabberd/ejabberd_acl.yml', 'w');
+      if (!$map_handle) {
+        throw new Exception($lang['danger']['file_open_error']);
+      }
+      fwrite($map_handle, '# Autogenerated by mailcow' . PHP_EOL);
+      fwrite($map_handle, 'append_host_config:' . PHP_EOL);
+      foreach ($xmpp_domains as $domain => $domain_values) {
+        fwrite($map_handle, '  ' . $xmpp_domains[$domain]['xmpp_host'] . ':' . PHP_EOL);
+        fwrite($map_handle, '    acl:' . PHP_EOL);
+        fwrite($map_handle, '      admin:' . PHP_EOL);
+        fwrite($map_handle, '        user:' . PHP_EOL);
+        foreach ($xmpp_domains[$domain]['xmpp_admins'] as $xmpp_admin) {
+          fwrite($map_handle, '          - ' . $xmpp_admin . PHP_EOL);
+        }
+      }
+      fclose($map_handle);
+    }
+    else {
+      // Write empty hosts file
+      $map_handle = fopen('/ejabberd/ejabberd_hosts.yml', 'w');
+      if (!$map_handle) {
+        throw new Exception($lang['danger']['file_open_error']);
+      }
+      fwrite($map_handle, '# Autogenerated by mailcow' . PHP_EOL);
+      fclose($map_handle);
+      // Write empty ACL file
+      $map_handle = fopen('/ejabberd/ejabberd_acl.yml', 'w');
+      if (!$map_handle) {
+        throw new Exception($lang['danger']['file_open_error']);
+      }
+      fwrite($map_handle, '# Autogenerated by mailcow' . PHP_EOL);
+      fclose($map_handle);
+    }
+
+    if (md5_file('/ejabberd/ejabberd_acl.yml') != $ejabberd_acl_md5) {
+      xmpp_control('restart');
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data_log),
+        'msg' => 'xmpp_maps_updated'
+      );
+    }
+    elseif (md5_file('/ejabberd/ejabberd_hosts.yml') != $ejabberd_hosts_md5) {
+      xmpp_control('reload');
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data_log),
+        'msg' => 'xmpp_maps_updated'
+      );
+    }
+
+  }
+  catch (Exception $e) {
+    $_SESSION['return'][] = array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_action, $_data_log),
+      'msg' => array('xmpp_map_write_error', htmlspecialchars($e->getMessage()))
+    );
+  }
+}
+

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

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
   try {
     global $pdo;
     global $pdo;
 
 
-    $db_version = "28112020_1210";
+    $db_version = "08022021_1000";
 
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -240,6 +240,8 @@ function init_db_schema() {
           "gal" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "gal" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "xmpp" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "xmpp_prefix" => "VARCHAR(255) DEFAULT 'im'",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
@@ -567,6 +569,10 @@ function init_db_schema() {
           "protocol_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "protocol_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "smtp_ip_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "smtp_ip_access" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "xmpp_prefix" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "xmpp_domain_access" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "xmpp_mailbox_access" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "xmpp_admin" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'"
           "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'"
           ),
           ),
         "keys" => array(
         "keys" => array(
@@ -1179,6 +1185,8 @@ function init_db_schema() {
     $pdo->query("UPDATE `pushover` SET `attributes` =  JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;");
     $pdo->query("UPDATE `pushover` SET `attributes` =  JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;");
     // mailbox
     // mailbox
     $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;");
+    $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.xmpp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.xmpp_access') IS NULL;");
+    $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.xmpp_admin', \"0\") WHERE JSON_VALUE(`attributes`, '$.xmpp_admin') IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;");
     $pdo->query("UPDATE `mailbox` SET `attributes` =  JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;");
@@ -1226,6 +1234,7 @@ function init_db_schema() {
 }
 }
 if (php_sapi_name() == "cli") {
 if (php_sapi_name() == "cli") {
   include '/web/inc/vars.inc.php';
   include '/web/inc/vars.inc.php';
+  include '/web/inc/functions.xmpp.inc.php';
   // $now = new DateTime();
   // $now = new DateTime();
   // $mins = $now->getOffset() / 60;
   // $mins = $now->getOffset() / 60;
   // $sgn = ($mins < 0 ? -1 : 1);
   // $sgn = ($mins < 0 ? -1 : 1);
@@ -1264,5 +1273,7 @@ if (php_sapi_name() == "cli") {
   catch ( Exception $e ) {
   catch ( Exception $e ) {
     // Dunno
     // Dunno
   }
   }
+  xmpp_rebuild_configs();
+  echo "Rebuilt XMPP configuration". PHP_EOL;
   init_db_schema();
   init_db_schema();
 }
 }

+ 12 - 11
data/web/inc/prerequisites.inc.php

@@ -234,27 +234,28 @@ if(file_exists($langFile)) {
 }
 }
 
 
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.acl.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.acl.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.app_passwd.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.address_rewriting.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.address_rewriting.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.admin.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.admin.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantine.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quota_notification.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.app_passwd.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailq.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailq.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.oauth2.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.oauth2.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.presets.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.pushover.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.pushover.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantine.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quota_notification.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.transports.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rspamd.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rspamd.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.presets.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.transports.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.xmpp.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
 init_db_schema();
 init_db_schema();

+ 10 - 7
data/web/inc/spf.inc.php

@@ -1,8 +1,6 @@
 <?php
 <?php
 error_reporting(0);
 error_reporting(0);
-
-function get_spf_allowed_hosts($check_domain)
-{
+function get_spf_allowed_hosts($check_domain, $expand_ipv6 = false) {
 	$hosts = array();
 	$hosts = array();
 	
 	
 	$records = dns_get_record($check_domain, DNS_TXT);
 	$records = dns_get_record($check_domain, DNS_TXT);
@@ -81,7 +79,13 @@ function get_spf_allowed_hosts($check_domain)
 	}
 	}
 	foreach ($hosts as &$host) {
 	foreach ($hosts as &$host) {
 		if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
 		if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
-			$host = $host;
+      if ($expand_ipv6 === true) {
+        $hex = unpack("H*hex", inet_pton($host));
+        $host = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
+      }
+      else {
+        $host = $host;
+      }
 		}
 		}
 	}
 	}
 	return $hosts;
 	return $hosts;
@@ -119,9 +123,8 @@ function get_a_hosts($domain)
 		$hosts[] = $a_record['ip'];
 		$hosts[] = $a_record['ip'];
 	}
 	}
 	$a_records = dns_get_record($domain, DNS_AAAA);
 	$a_records = dns_get_record($domain, DNS_AAAA);
-	foreach ($a_records as $a_record)
-	{
-		$hosts[] = $a_record['ipv6'];
+	foreach ($a_records as $a_record) {
+    $hosts[] = $a_record['ipv6'];
 	}
 	}
 	
 	
 	return $hosts;
 	return $hosts;

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

@@ -167,6 +167,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['pop3_access'] = true;
 // Mailbox has SMTP access by default
 // Mailbox has SMTP access by default
 $MAILBOX_DEFAULT_ATTRIBUTES['smtp_access'] = true;
 $MAILBOX_DEFAULT_ATTRIBUTES['smtp_access'] = true;
 
 
+// Mailbox has XMPP access by default (if domain has XMPP enabled)
+$MAILBOX_DEFAULT_ATTRIBUTES['xmpp_access'] = true;
+
+// Mailbox is XMPP admin by default (bad)
+$MAILBOX_DEFAULT_ATTRIBUTES['xmpp_admin'] = false;
+
 // Mailbox receives notifications about...
 // Mailbox receives notifications about...
 // "add_header" - mail that was put into the Junk folder
 // "add_header" - mail that was put into the Junk folder
 // "reject" - mail that was rejected
 // "reject" - mail that was rejected

+ 2 - 0
data/web/js/build/014-mailcow.js

@@ -189,6 +189,8 @@ $(document).ready(function() {
       $(this).attr("disabled", true);
       $(this).attr("disabled", true);
     } else if ($(this).attr('data-provide') == 'slider') {
     } else if ($(this).attr('data-provide') == 'slider') {
       $(this).slider("disable");
       $(this).slider("disable");
+    } else if ($(this).is(':checkbox')) {
+      $(this).attr("disabled", true);
     }
     }
     $(this).data("toggle", "tooltip");
     $(this).data("toggle", "tooltip");
     $(this).attr("title", lang_acl.prohibited);
     $(this).attr("title", lang_acl.prohibited);

+ 40 - 1
data/web/js/site/debug.js

@@ -1,3 +1,38 @@
+$(document).ready(function() {
+  // Parse seconds ago to date
+  // Get "now" timestamp
+  var ts_now = Math.round((new Date()).getTime() / 1000);
+  $('.parse_s_ago').each(function(i, parse_s_ago) {
+    var started_s_ago = parseInt($(this).text(), 10);
+    if (typeof started_s_ago != 'NaN') {
+      var started_date = new Date((ts_now - started_s_ago) * 1000);
+      var started_local_date = started_date.toLocaleDateString(undefined, {
+        year: "numeric",
+        month: "2-digit",
+        day: "2-digit",
+        hour: "2-digit",
+        minute: "2-digit",
+        second: "2-digit"
+      });
+      $(this).text(started_local_date);
+    }
+  });
+  // Parse general dates
+  $('.parse_date').each(function(i, parse_date) {
+    var started_date = new Date(Date.parse($(this).text()));
+    if (typeof started_date != 'NaN') {
+      var started_local_date = started_date.toLocaleDateString(undefined, {
+        year: "numeric",
+        month: "2-digit",
+        day: "2-digit",
+        hour: "2-digit",
+        minute: "2-digit",
+        second: "2-digit"
+      });
+      $(this).text(started_local_date);
+    }
+  });
+});
 jQuery(function($){
 jQuery(function($){
   if (localStorage.getItem("current_page") === null) {
   if (localStorage.getItem("current_page") === null) {
     var current_page = {};
     var current_page = {};
@@ -631,7 +666,11 @@ jQuery(function($){
       $.each(data, function (i, item) {
       $.each(data, function (i, item) {
         if (item === null) { return true; }
         if (item === null) { return true; }
         if (item.message.match("^base64,")) {
         if (item.message.match("^base64,")) {
-          item.message = atob(item.message.slice(7)).replace(/\\n/g, "<br />");
+          try {
+            item.message = atob(item.message.slice(7)).replace(/\\n/g, "<br />");
+          } catch(e) {
+            item.message = item.message.slice(7);
+          }
         } else {
         } else {
           item.message = escapeHtml(item.message);
           item.message = escapeHtml(item.message);
         }
         }

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

@@ -256,6 +256,7 @@ jQuery(function($){
         {"name":"rl","title":"RL","breakpoints":"xs sm md lg","style":{"maxWidth":"100px","width":"100px"}},
         {"name":"rl","title":"RL","breakpoints":"xs sm md lg","style":{"maxWidth":"100px","width":"100px"}},
         {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md lg","formatter": function(value){return 1==value?'&#10003;':0==value&&'&#10005;';}},
         {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md lg","formatter": function(value){return 1==value?'&#10003;':0==value&&'&#10005;';}},
         {"name":"domain_admins","title":lang.domain_admins,"style":{"word-break":"break-all","min-width":"200px"},"breakpoints":"xs sm md lg","filterable":(role == "admin"),"visible":(role == "admin")},
         {"name":"domain_admins","title":lang.domain_admins,"style":{"word-break":"break-all","min-width":"200px"},"breakpoints":"xs sm md lg","filterable":(role == "admin"),"visible":(role == "admin")},
+        {"name":"xmpp","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":"XMPP","formatter": function(value){return 1==value?'&#10003;':0==value&&'&#10005;';}},
         {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'&#10003;':0==value&&'&#10005;';}},
         {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'&#10003;':0==value&&'&#10005;';}},
         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
       ],
       ],

+ 5 - 5
data/web/lang/lang.cs.json

@@ -435,12 +435,12 @@
         "logs": "Logy",
         "logs": "Logy",
         "restart_container": "Restartovat",
         "restart_container": "Restartovat",
         "solr_dead": "Solr se spouští, je vypnutý nebo spadl.",
         "solr_dead": "Solr se spouští, je vypnutý nebo spadl.",
-        "solr_docs": "Dokumentace",
-        "solr_last_modified": "Naposledy změněn",
-        "solr_size": "Velikost",
-        "solr_started_at": "Spuštěn",
+        "docs": "Dokumentace",
+        "last_modified": "Naposledy změněn",
+        "size": "Velikost",
+        "started_at": "Spuštěn",
         "solr_status": "Stav Solr",
         "solr_status": "Stav Solr",
-        "solr_uptime": "Doba běhu",
+        "uptime": "Doba běhu",
         "started_on": "Spuštěno",
         "started_on": "Spuštěno",
         "static_logs": "Statické logy",
         "static_logs": "Statické logy",
         "system_containers": "Systém a kontejnery"
         "system_containers": "Systém a kontejnery"

+ 39 - 9
data/web/lang/lang.de.json

@@ -26,7 +26,10 @@
         "syncjobs": "Sync Jobs",
         "syncjobs": "Sync Jobs",
         "tls_policy": "Verschlüsselungsrichtlinie",
         "tls_policy": "Verschlüsselungsrichtlinie",
         "unlimited_quota": "Unendliche Quota für Mailboxen",
         "unlimited_quota": "Unendliche Quota für Mailboxen",
-        "domain_desc": "Domainbeschreibung ändern"
+        "domain_desc": "Domainbeschreibung ändern",
+        "xmpp_admin": "Benutzer zum XMPP-Administrator ernennen",
+        "xmpp_access": "XMPP-Zugang eines Benutzers einstellen",
+        "xmpp_prefix": "XMPP-Subdomain ändern"
     },
     },
     "add": {
     "add": {
         "activate_filter_warn": "Alle anderen Filter diesen Typs werden deaktiviert, falls dieses Script aktiv markiert wird.",
         "activate_filter_warn": "Alle anderen Filter diesen Typs werden deaktiviert, falls dieses Script aktiv markiert wird.",
@@ -59,6 +62,14 @@
         "full_name": "Vor- und Nachname",
         "full_name": "Vor- und Nachname",
         "gal": "Globales Adressbuch",
         "gal": "Globales Adressbuch",
         "gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich! <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
         "gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich! <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
+        "xmpp": "XMPP für diese Domain aktivieren",
+        "xmpp_prefix": "XMPP-Prefix für Domain (\"im\" für <b>im</b>.example.org)",
+        "xmpp_prefix_info": "Für die Bereitstellung eines Zertifikates sollte vorab ein DNS-Eintrag, etwa in Form eines CNAMEs, für <b>im</b>.example.org sowie <b>*.im</b>.example.org auf <b>%s</b> zeigend angelegt werden. Im Anschluss an die Aktivierung sollte der DNS-Check für diese Domain ausgeführt werden.",
+        "xmpp_info": "Diese Funktion stellt eine Chat-Funktionalität für die Domain bereit.",
+        "xmpp_access": "XMPP Zugang",
+        "xmpp_access_info": "XMPP muss für diese Domain aktiviert sein.",
+        "xmpp_admin": "XMPP Administrator",
+        "xmpp_admin_info": "<b>Vorsicht:</b> Ernennt den Benutzer zum Administrator der jeweiligen XMPP Domain.",
         "generate": "generieren",
         "generate": "generieren",
         "goto_ham": "Nachrichten als <span class=\"text-success\"><b>Ham</b></span> lernen",
         "goto_ham": "Nachrichten als <span class=\"text-success\"><b>Ham</b></span> lernen",
         "goto_null": "Nachrichten sofort verwerfen",
         "goto_null": "Nachrichten sofort verwerfen",
@@ -350,6 +361,7 @@
         "global_filter_write_error": "Kann Filterdatei nicht schreiben: %s",
         "global_filter_write_error": "Kann Filterdatei nicht schreiben: %s",
         "global_map_invalid": "Rspamd Map %s ist ungültig",
         "global_map_invalid": "Rspamd Map %s ist ungültig",
         "global_map_write_error": "Kann globale Map ID %s nicht schreiben: %s",
         "global_map_write_error": "Kann globale Map ID %s nicht schreiben: %s",
+        "xmpp_map_write_error": "Kann XMPP Map nicht schreiben: %s",
         "goto_empty": "Eine Alias-Adresse muss auf mindestens eine gütlige Ziel-Adresse zeigen",
         "goto_empty": "Eine Alias-Adresse muss auf mindestens eine gütlige Ziel-Adresse zeigen",
         "goto_invalid": "Ziel-Adresse %s ist ungültig",
         "goto_invalid": "Ziel-Adresse %s ist ungültig",
         "ham_learn_error": "Ham Lernfehler: %s",
         "ham_learn_error": "Ham Lernfehler: %s",
@@ -434,7 +446,9 @@
         "username_invalid": "Benutzername %s kann nicht verwendet werden",
         "username_invalid": "Benutzername %s kann nicht verwendet werden",
         "validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
         "validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
         "value_missing": "Bitte alle Felder ausfüllen",
         "value_missing": "Bitte alle Felder ausfüllen",
-        "yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s"
+        "yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
+        "xmpp_restart_failed": "XMPP konnte nicht neu gestartet werden",
+        "xmpp_reload_failed": "XMPP konnte nicht neu geladen werden"
     },
     },
     "debug": {
     "debug": {
         "chart_this_server": "Chart (dieser Server)",
         "chart_this_server": "Chart (dieser Server)",
@@ -448,15 +462,18 @@
         "logs": "Protokolle",
         "logs": "Protokolle",
         "restart_container": "Neustart",
         "restart_container": "Neustart",
         "solr_dead": "Solr startet, ist deaktiviert oder temporär nicht erreichbar.",
         "solr_dead": "Solr startet, ist deaktiviert oder temporär nicht erreichbar.",
-        "solr_docs": "Dokumente",
-        "solr_last_modified": "Zuletzt geändert",
-        "solr_size": "Größe",
-        "solr_started_at": "Gestartet am",
+        "xmpp_dead": "XMPP startet, ist deaktiviert oder temporär nicht erreichbar.",
+        "docs": "Dokumente",
+        "last_modified": "Zuletzt geändert",
+        "online_users": "Benutzer online",
+        "size": "Größe",
+        "started_at": "Gestartet am",
         "solr_status": "Solr Status",
         "solr_status": "Solr Status",
-        "solr_uptime": "Uptime",
+        "uptime": "Uptime",
         "started_on": "Gestartet am",
         "started_on": "Gestartet am",
         "static_logs": "Statische Logs",
         "static_logs": "Statische Logs",
-        "system_containers": "System & Container"
+        "system_containers": "System & Container",
+        "xmpp_status": "XMPP Status"
     },
     },
     "diagnostics": {
     "diagnostics": {
         "cname_from_a": "Wert abgeleitet von A/AAAA-Eintrag. Wird unterstützt, sofern der Eintrag auf die korrekte Ressource zeigt.",
         "cname_from_a": "Wert abgeleitet von A/AAAA-Eintrag. Wird unterstützt, sofern der Eintrag auf die korrekte Ressource zeigt.",
@@ -505,6 +522,14 @@
         "full_name": "Voller Name",
         "full_name": "Voller Name",
         "gal": "Globales Adressbuch",
         "gal": "Globales Adressbuch",
         "gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
         "gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
+        "xmpp": "XMPP für diese Domain aktivieren",
+        "xmpp_prefix": "XMPP-Prefix für Domain (\"im\" für <b>im</b>.example.org)",
+        "xmpp_prefix_info": "Für die Bereitstellung eines Zertifikates sollte vorab ein DNS-Eintrag, etwa in Form eines CNAMEs, für <b>im</b>.example.org sowie <b>*.im</b>.example.org auf <b>%s</b> zeigend angelegt werden. Im Anschluss an die Aktivierung sollte der DNS-Check für diese Domain ausgeführt werden.",
+        "xmpp_info": "Diese Funktion stellt eine Chat-Funktionalität für die Domain bereit.",
+        "xmpp_access": "XMPP Zugang",
+        "xmpp_access_info": "XMPP muss für diese Domain aktiviert sein.",
+        "xmpp_admin": "XMPP Administrator",
+        "xmpp_admin_info": "<b>Vorsicht:</b> Ernennt den Benutzer zum Administrator der jeweiligen XMPP Domain.",
         "generate": "generieren",
         "generate": "generieren",
         "grant_types": "Grant types",
         "grant_types": "Grant types",
         "hostname": "Servername",
         "hostname": "Servername",
@@ -536,6 +561,7 @@
         "pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.<br>Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie <b>hängen nicht voneinander ab</b> und werden der Reihe nach ausgeführt. <br>Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)",
         "pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.<br>Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie <b>hängen nicht voneinander ab</b> und werden der Reihe nach ausgeführt. <br>Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)",
         "pushover_verify": "Verbindung verifizieren",
         "pushover_verify": "Verbindung verifizieren",
         "quota_mb": "Speicherplatz (MiB)",
         "quota_mb": "Speicherplatz (MiB)",
+        "ratelimit": "Rate Limit",
         "redirect_uri": "Redirect/Callback-URL",
         "redirect_uri": "Redirect/Callback-URL",
         "relay_all": "Alle Empfänger-Adressen relayen",
         "relay_all": "Alle Empfänger-Adressen relayen",
         "relay_all_info": "↪ Wenn <b>nicht</b> alle Empfänger-Adressen relayt werden sollen, müssen \"blinde\" Mailboxen für jede Adresse, die relayt werden soll, erstellen werden.",
         "relay_all_info": "↪ Wenn <b>nicht</b> alle Empfänger-Adressen relayt werden sollen, müssen \"blinde\" Mailboxen für jede Adresse, die relayt werden soll, erstellen werden.",
@@ -558,6 +584,7 @@
         "sogo_visible": "Alias in SOGo sichtbar",
         "sogo_visible": "Alias in SOGo sichtbar",
         "sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
         "sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
         "spam_alias": "Anpassen temporärer Alias-Adressen",
         "spam_alias": "Anpassen temporärer Alias-Adressen",
+        "spam_filter": "Spamfilter",
         "spam_policy": "Hinzufügen und Entfernen von Einträgen in White- und Blacklists",
         "spam_policy": "Hinzufügen und Entfernen von Einträgen in White- und Blacklists",
         "spam_score": "Einen benutzerdefiniterten Spam-Score festlegen",
         "spam_score": "Einen benutzerdefiniterten Spam-Score festlegen",
         "subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
         "subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
@@ -891,7 +918,10 @@
         "verified_totp_login": "TOTP-Anmeldung verifiziert",
         "verified_totp_login": "TOTP-Anmeldung verifiziert",
         "verified_u2f_login": "U2F-Anmeldung verifiziert",
         "verified_u2f_login": "U2F-Anmeldung verifiziert",
         "verified_fido2_login": "FIDO2-Anmeldung verifiziert",
         "verified_fido2_login": "FIDO2-Anmeldung verifiziert",
-        "verified_yotp_login": "Yubico OTP-Anmeldung verifiziert"
+        "verified_yotp_login": "Yubico OTP-Anmeldung verifiziert",
+        "xmpp_restarted": "XMPP-Dienst wurde neu gestartet",
+        "xmpp_reloaded": "XMPP-Dienst wurde neu geladen",
+        "xmpp_maps_updated": "XMPP-Maps wurden aktualisiert"
     },
     },
     "tfa": {
     "tfa": {
         "api_register": "%s verwendet die Yubico Cloud API. Ein API-Key für den Yubico Stick kann <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">hier</a> bezogen werden.",
         "api_register": "%s verwendet die Yubico Cloud API. Ein API-Key für den Yubico Stick kann <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">hier</a> bezogen werden.",

+ 37 - 9
data/web/lang/lang.en.json

@@ -26,7 +26,10 @@
         "syncjobs": "Sync jobs",
         "syncjobs": "Sync jobs",
         "tls_policy": "TLS policy",
         "tls_policy": "TLS policy",
         "unlimited_quota": "Unlimited quota for mailboxes",
         "unlimited_quota": "Unlimited quota for mailboxes",
-        "domain_desc": "Change domain description"
+        "domain_desc": "Change domain description",
+        "xmpp_admin": "Change XMPP admin status of a user",
+        "xmpp_access": "Change XMPP access for a user",
+        "xmpp_prefix": "Change XMPP subdomain"
     },
     },
     "add": {
     "add": {
         "activate_filter_warn": "All other filters will be deactivated, when active is checked.",
         "activate_filter_warn": "All other filters will be deactivated, when active is checked.",
@@ -59,6 +62,12 @@
         "full_name": "Full name",
         "full_name": "Full name",
         "gal": "Global Address List",
         "gal": "Global Address List",
         "gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
         "gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
+        "xmpp": "Activate XMPP for this domain",
+        "xmpp_info": "This function will enable chat functionality for this domain.",
+        "xmpp_access": "XMPP access",
+        "xmpp_access_info": "XMPP must be enabled for this domain.",
+        "xmpp_admin": "XMPP administrator",
+        "xmpp_admin_info": "<b>Danger:</b> Promotes a user to an XMPP administrator of this domain.",
         "generate": "generate",
         "generate": "generate",
         "goto_ham": "Learn as <span class=\"text-success\"><b>ham</b></span>",
         "goto_ham": "Learn as <span class=\"text-success\"><b>ham</b></span>",
         "goto_null": "Silently discard mail",
         "goto_null": "Silently discard mail",
@@ -353,6 +362,7 @@
         "global_filter_write_error": "Could not write filter file: %s",
         "global_filter_write_error": "Could not write filter file: %s",
         "global_map_invalid": "Global map ID %s invalid",
         "global_map_invalid": "Global map ID %s invalid",
         "global_map_write_error": "Could not write global map ID %s: %s",
         "global_map_write_error": "Could not write global map ID %s: %s",
+        "xmpp_map_write_error": "Could not write XMPP map: %s",
         "goto_empty": "An alias address must contain at least one valid goto address",
         "goto_empty": "An alias address must contain at least one valid goto address",
         "goto_invalid": "Goto address %s is invalid",
         "goto_invalid": "Goto address %s is invalid",
         "ham_learn_error": "Ham learn error: %s",
         "ham_learn_error": "Ham learn error: %s",
@@ -437,7 +447,9 @@
         "username_invalid": "Username %s cannot be used",
         "username_invalid": "Username %s cannot be used",
         "validity_missing": "Please assign a period of validity",
         "validity_missing": "Please assign a period of validity",
         "value_missing": "Please provide all values",
         "value_missing": "Please provide all values",
-        "yotp_verification_failed": "Yubico OTP verification failed: %s"
+        "yotp_verification_failed": "Yubico OTP verification failed: %s",
+        "xmpp_restart_failed": "XMPP could not be restarted",
+        "xmpp_reload_failed": "XMPP could not be reloaded"
     },
     },
     "debug": {
     "debug": {
         "chart_this_server": "Chart (this server)",
         "chart_this_server": "Chart (this server)",
@@ -451,15 +463,18 @@
         "logs": "Logs",
         "logs": "Logs",
         "restart_container": "Restart",
         "restart_container": "Restart",
         "solr_dead": "Solr is starting, disabled or died.",
         "solr_dead": "Solr is starting, disabled or died.",
-        "solr_docs": "Docs",
-        "solr_last_modified": "Last modified",
-        "solr_size": "Size",
-        "solr_started_at": "Started at",
+        "xmpp_dead": "XMPP is starting, disabled or died.",
+        "docs": "Docs",
+        "last_modified": "Last modified",
+        "online_users": "Users online",
+        "size": "Size",
+        "started_at": "Started at",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
-        "solr_uptime": "Uptime",
+        "uptime": "Uptime",
         "started_on": "Started on",
         "started_on": "Started on",
         "static_logs": "Static logs",
         "static_logs": "Static logs",
-        "system_containers": "System & Containers"
+        "system_containers": "System & Containers",
+        "xmpp_status": "XMPP status"
     },
     },
     "diagnostics": {
     "diagnostics": {
         "cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.",
         "cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.",
@@ -508,6 +523,14 @@
         "full_name": "Full name",
         "full_name": "Full name",
         "gal": "Global Address List",
         "gal": "Global Address List",
         "gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
         "gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
+        "xmpp": "Activate XMPP for this domain",
+        "xmpp_prefix": "XMPP prefix for domain (\"im\" to use <b>im</b>.example.org)",
+        "xmpp_prefix_info": "To request certificates for XMPP, two CNAME DNS records should point from <b>im</b>.example.org as well as <b>*.im</b>.example.org to <b>%s</b>. Please also run the DNS check for this domain after enabling XMPP.",
+        "xmpp_info": "This function will enable chat functionality for this domain.",
+        "xmpp_access": "XMPP access",
+        "xmpp_access_info": "XMPP must be enabled for this domain.",
+        "xmpp_admin": "XMPP administrator",
+        "xmpp_admin_info": "<b>Danger:</b> Promotes a user to an XMPP administrator of this domain.",
         "generate": "generate",
         "generate": "generate",
         "grant_types": "Grant types",
         "grant_types": "Grant types",
         "hostname": "Hostname",
         "hostname": "Hostname",
@@ -539,6 +562,7 @@
         "pushover_vars": "When no sender filter is defined, all mails will be considered.<br>Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.<br>Useable variables for text and title (please take note of data protection policies)",
         "pushover_vars": "When no sender filter is defined, all mails will be considered.<br>Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.<br>Useable variables for text and title (please take note of data protection policies)",
         "pushover_verify": "Verify credentials",
         "pushover_verify": "Verify credentials",
         "quota_mb": "Quota (MiB)",
         "quota_mb": "Quota (MiB)",
+        "ratelimit": "Rate limit",
         "redirect_uri": "Redirect/Callback URL",
         "redirect_uri": "Redirect/Callback URL",
         "relay_all": "Relay all recipients",
         "relay_all": "Relay all recipients",
         "relay_all_info": "↪ If you choose <b>not</b> to relay all recipients, you will need to add a (\"blind\") mailbox for every single recipient that should be relayed.",
         "relay_all_info": "↪ If you choose <b>not</b> to relay all recipients, you will need to add a (\"blind\") mailbox for every single recipient that should be relayed.",
@@ -561,6 +585,7 @@
         "sogo_visible": "Alias is visible in SOGo",
         "sogo_visible": "Alias is visible in SOGo",
         "sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
         "sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
         "spam_alias": "Create or change time limited alias addresses",
         "spam_alias": "Create or change time limited alias addresses",
+        "spam_filter": "Spam filter",
         "spam_policy": "Add or remove items to white-/blacklist",
         "spam_policy": "Add or remove items to white-/blacklist",
         "spam_score": "Set a custom spam score",
         "spam_score": "Set a custom spam score",
         "subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
         "subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
@@ -894,7 +919,10 @@
         "verified_totp_login": "Verified TOTP login",
         "verified_totp_login": "Verified TOTP login",
         "verified_u2f_login": "Verified U2F login",
         "verified_u2f_login": "Verified U2F login",
         "verified_fido2_login": "Verified FIDO2 login",
         "verified_fido2_login": "Verified FIDO2 login",
-        "verified_yotp_login": "Verified Yubico OTP login"
+        "verified_yotp_login": "Verified Yubico OTP login",
+        "xmpp_restarted": "XMPP service was restarted",
+        "xmpp_reloaded": "XMPP service was reloaded",
+        "xmpp_maps_updated": "XMPP maps were updated"
     },
     },
     "tfa": {
     "tfa": {
         "api_register": "%s uses the Yubico Cloud API. Please get an API key for your key <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
         "api_register": "%s uses the Yubico Cloud API. Please get an API key for your key <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",

+ 5 - 5
data/web/lang/lang.es.json

@@ -344,12 +344,12 @@
         "logs": "Logs",
         "logs": "Logs",
         "restart_container": "Reiniciar",
         "restart_container": "Reiniciar",
         "solr_dead": "Solr está empezando, deshabilitado o caído.",
         "solr_dead": "Solr está empezando, deshabilitado o caído.",
-        "solr_docs": "Docs",
-        "solr_last_modified": "Última modificación",
-        "solr_size": "Tamaño",
-        "solr_started_at": "Iniciado el",
+        "docs": "Docs",
+        "last_modified": "Última modificación",
+        "size": "Tamaño",
+        "started_at": "Iniciado el",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
-        "solr_uptime": "Uptime",
+        "uptime": "Uptime",
         "static_logs": "Logs estáticos",
         "static_logs": "Logs estáticos",
         "system_containers": "Sistema y Contenedores"
         "system_containers": "Sistema y Contenedores"
     },
     },

+ 5 - 5
data/web/lang/lang.fi.json

@@ -389,12 +389,12 @@
         "logs": "Logit tausta palveluista",
         "logs": "Logit tausta palveluista",
         "restart_container": "Uudelleen käynnistä",
         "restart_container": "Uudelleen käynnistä",
         "solr_dead": "Solr käynnistyy, on poissa käytöstä tai kuoli.",
         "solr_dead": "Solr käynnistyy, on poissa käytöstä tai kuoli.",
-        "solr_docs": "Docs",
-        "solr_last_modified": "Viimeksi muokattu",
-        "solr_size": "Koko",
-        "solr_started_at": "Käynnistetty",
+        "docs": "Docs",
+        "last_modified": "Viimeksi muokattu",
+        "size": "Koko",
+        "started_at": "Käynnistetty",
         "solr_status": "Solr-tila",
         "solr_status": "Solr-tila",
-        "solr_uptime": "Päällä",
+        "uptime": "Päällä",
         "started_on": "Aloitettiin",
         "started_on": "Aloitettiin",
         "static_logs": "Staattiset lokit",
         "static_logs": "Staattiset lokit",
         "system_containers": "Systeemi & Säiliöt"
         "system_containers": "Systeemi & Säiliöt"

+ 5 - 5
data/web/lang/lang.fr.json

@@ -438,12 +438,12 @@
 		"logs": "Logs",
 		"logs": "Logs",
 		"restart_container": "Redémarrer",
 		"restart_container": "Redémarrer",
 		"solr_dead": "Solr est en cours de démarrage, désactivé ou mort.",
 		"solr_dead": "Solr est en cours de démarrage, désactivé ou mort.",
-		"solr_docs": "Docs",
-		"solr_last_modified": "Dernière modification",
-		"solr_size": "Taille",
-		"solr_started_at": "Démarré à",
+		"docs": "Docs",
+		"last_modified": "Dernière modification",
+		"size": "Taille",
+		"started_at": "Démarré à",
 		"solr_status": "Etat Solr",
 		"solr_status": "Etat Solr",
-		"solr_uptime": "Disponibilité",
+		"uptime": "Disponibilité",
 		"started_on": "Démarré à",
 		"started_on": "Démarré à",
 		"static_logs": "Logs statiques",
 		"static_logs": "Logs statiques",
 		"system_containers": "Système & Conteneurs"
 		"system_containers": "Système & Conteneurs"

+ 5 - 5
data/web/lang/lang.ko.json

@@ -438,12 +438,12 @@
         "logs": "Logs",
         "logs": "Logs",
         "restart_container": "Restart",
         "restart_container": "Restart",
         "solr_dead": "Solr is starting, disabled or died.",
         "solr_dead": "Solr is starting, disabled or died.",
-        "solr_docs": "Docs",
-        "solr_last_modified": "Last modified",
-        "solr_size": "Size",
-        "solr_started_at": "Started at",
+        "docs": "Docs",
+        "last_modified": "Last modified",
+        "size": "Size",
+        "started_at": "Started at",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
-        "solr_uptime": "Uptime",
+        "uptime": "Uptime",
         "started_on": "Started on",
         "started_on": "Started on",
         "static_logs": "Static logs",
         "static_logs": "Static logs",
         "system_containers": "System & Containers"
         "system_containers": "System & Containers"

+ 5 - 5
data/web/lang/lang.nl.json

@@ -449,12 +449,12 @@
         "logs": "Logs",
         "logs": "Logs",
         "restart_container": "Herstart",
         "restart_container": "Herstart",
         "solr_dead": "Solr is uitgeschakeld, uitgevallen of nog bezig met opstarten.",
         "solr_dead": "Solr is uitgeschakeld, uitgevallen of nog bezig met opstarten.",
-        "solr_docs": "Documenten",
-        "solr_last_modified": "Voor het laatst bijgewerkt op",
-        "solr_size": "Grootte",
-        "solr_started_at": "Opgestart op",
+        "docs": "Documenten",
+        "last_modified": "Voor het laatst bijgewerkt op",
+        "size": "Grootte",
+        "started_at": "Opgestart op",
         "solr_status": "Status van Solr",
         "solr_status": "Status van Solr",
-        "solr_uptime": "Uptime",
+        "uptime": "Uptime",
         "started_on": "Gestart op",
         "started_on": "Gestart op",
         "static_logs": "Statische logs",
         "static_logs": "Statische logs",
         "system_containers": "Systeem & containers"
         "system_containers": "Systeem & containers"

+ 5 - 5
data/web/lang/lang.ro.json

@@ -451,12 +451,12 @@
         "logs": "Jurnale",
         "logs": "Jurnale",
         "restart_container": "Repornire",
         "restart_container": "Repornire",
         "solr_dead": "Solr începe, este invalid sau s-a oprit.",
         "solr_dead": "Solr începe, este invalid sau s-a oprit.",
-        "solr_docs": "Documente",
-        "solr_last_modified": "Ultima modificare",
-        "solr_size": "Mărime",
-        "solr_started_at": "Pornit la",
+        "docs": "Documente",
+        "last_modified": "Ultima modificare",
+        "size": "Mărime",
+        "started_at": "Pornit la",
         "solr_status": "Stare Solr",
         "solr_status": "Stare Solr",
-        "solr_uptime": "Timp de funcționare",
+        "uptime": "Timp de funcționare",
         "started_on": "Început pe",
         "started_on": "Început pe",
         "static_logs": "Jurnale statice",
         "static_logs": "Jurnale statice",
         "system_containers": "Sistem și Containere"
         "system_containers": "Sistem și Containere"

+ 5 - 5
data/web/lang/lang.ru.json

@@ -451,12 +451,12 @@
         "logs": "Журналы",
         "logs": "Журналы",
         "restart_container": "Перезапустить",
         "restart_container": "Перезапустить",
         "solr_dead": "Solr не запущен. Если вы включили Solf в файле настроек <code>mailcow.conf</code> и это сообщение отображает более получаса, скорее всего Solr сломан.",
         "solr_dead": "Solr не запущен. Если вы включили Solf в файле настроек <code>mailcow.conf</code> и это сообщение отображает более получаса, скорее всего Solr сломан.",
-        "solr_docs": "Проиндексировано обьектов",
-        "solr_last_modified": "Последние изменения",
-        "solr_size": "Индексы занимают",
-        "solr_started_at": "Запущен",
+        "docs": "Проиндексировано обьектов",
+        "last_modified": "Последние изменения",
+        "size": "Индексы занимают",
+        "started_at": "Запущен",
         "solr_status": "Состояние Solr",
         "solr_status": "Состояние Solr",
-        "solr_uptime": "Время работы",
+        "uptime": "Время работы",
         "started_on": "Запущен в",
         "started_on": "Запущен в",
         "static_logs": "Статические журналы",
         "static_logs": "Статические журналы",
         "system_containers": "Система и контейнеры"
         "system_containers": "Система и контейнеры"

+ 5 - 5
data/web/lang/lang.sk.json

@@ -448,12 +448,12 @@
         "logs": "Správy",
         "logs": "Správy",
         "restart_container": "Reštartovať",
         "restart_container": "Reštartovať",
         "solr_dead": "Solr štartuje, bol vypnutý alebo zlyhal.",
         "solr_dead": "Solr štartuje, bol vypnutý alebo zlyhal.",
-        "solr_docs": "Dokumenty",
-        "solr_last_modified": "Naposledy upravené",
-        "solr_size": "Veľkosť",
-        "solr_started_at": "Spustený",
+        "docs": "Dokumenty",
+        "last_modified": "Naposledy upravené",
+        "size": "Veľkosť",
+        "started_at": "Spustený",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
-        "solr_uptime": "Doba behu",
+        "uptime": "Doba behu",
         "started_on": "Spustený",
         "started_on": "Spustený",
         "static_logs": "Statické správy",
         "static_logs": "Statické správy",
         "system_containers": "Systém & Kontajnery"
         "system_containers": "Systém & Kontajnery"

+ 5 - 5
data/web/lang/lang.sv.json

@@ -448,12 +448,12 @@
         "logs": "Loggar",
         "logs": "Loggar",
         "restart_container": "Omstart",
         "restart_container": "Omstart",
         "solr_dead": "Solr är i uppstart, har inaktiveras eller är tillfälligt avstängd.",
         "solr_dead": "Solr är i uppstart, har inaktiveras eller är tillfälligt avstängd.",
-        "solr_docs": "Dokumentation",
-        "solr_last_modified": "Senast ändrad",
-        "solr_size": "Storlek",
-        "solr_started_at": "Startades kl.",
+        "docs": "Dokumentation",
+        "last_modified": "Senast ändrad",
+        "size": "Storlek",
+        "started_at": "Startades kl.",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
-        "solr_uptime": "Upptid",
+        "uptime": "Upptid",
         "started_on": "Startades",
         "started_on": "Startades",
         "static_logs": "Statiska loggar",
         "static_logs": "Statiska loggar",
         "system_containers": "System & behållare"
         "system_containers": "System & behållare"

+ 5 - 5
data/web/lang/lang.zh.json

@@ -445,12 +445,12 @@
         "logs": "日志",
         "logs": "日志",
         "restart_container": "重启",
         "restart_container": "重启",
         "solr_dead": "Solr在启动中、已关闭或已停止运行",
         "solr_dead": "Solr在启动中、已关闭或已停止运行",
-        "solr_docs": "文档",
-        "solr_last_modified": "最后修改",
-        "solr_size": "大小",
-        "solr_started_at": "开始于",
+        "docs": "文档",
+        "last_modified": "最后修改",
+        "size": "大小",
+        "started_at": "开始于",
         "solr_status": "Solr状态",
         "solr_status": "Solr状态",
-        "solr_uptime": "运行时间",
+        "uptime": "运行时间",
         "started_on": "启动于",
         "started_on": "启动于",
         "static_logs": "静态日志",
         "static_logs": "静态日志",
         "system_containers": "系统和容器"
         "system_containers": "系统和容器"