git subrepo commit mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "308860af"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "3f1a5af8"
git-subrepo: version:  "0.4.5"
  origin:   "???"
  commit:   "???"
Change-Id: I5d51c14b45db54fe706be40a591ddbfcea50d4b0
diff --git a/mailcow/src/mailcow-dockerized/data/web/templates/base.twig b/mailcow/src/mailcow-dockerized/data/web/templates/base.twig
index 79042d5..06c47bd 100644
--- a/mailcow/src/mailcow-dockerized/data/web/templates/base.twig
+++ b/mailcow/src/mailcow-dockerized/data/web/templates/base.twig
@@ -9,35 +9,48 @@
   <title>{{ ui_texts.title_name|raw }}</title>
 
   <link rel="stylesheet" href="{{ css_path }}">
-  {% if theme != 'lumen' %}
-  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/{{ theme }}/bootstrap.min.css">
-  {% endif %}
+  <script>
+    // check if darkmode is preferred by OS or set by localStorage
+    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && localStorage.getItem("theme") !== "light" ||
+        localStorage.getItem("theme") === "dark") {
+      var head  = document.getElementsByTagName('head')[0];
+      var link  = document.createElement('link');
+      link.id   = 'dark-mode-theme';
+      link.rel  = 'stylesheet';
+      link.type = 'text/css';
+      link.href = '/css/themes/mailcow-darkmode.css';
+      head.appendChild(link);
+    }
+  </script>
+
   <link rel="shortcut icon" href="/favicon.png" type="image/png">
   <link rel="icon" href="/favicon.png" type="image/png">
 </head>
-<body id="top">
+<body>
 <div class="overlay"></div>
 {% block navbar %}
-<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
+<nav class="navbar navbar-expand-lg navbar-light bg-light navbar-fixed-top p-0">
   <div class="container-fluid">
-    <div class="navbar-header">
-      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-        <span class="icon-bar"></span>
-        <span class="icon-bar"></span>
-        <span class="icon-bar"></span>
-      </button>
-      <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}"></a>
-    </div>
+    <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}"></a>
+    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
+      <i class="bi bi-list fs-3"></i>
+    </button>
     <div id="navbar" class="navbar-collapse collapse">
-      <ul class="nav navbar-nav navbar-right">
+      <ul class="navbar-nav ms-auto">
+        <li class="nav-item">
+          <div class="nav-link form-check form-switch my-auto d-flex align-items-center">
+            <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
+            <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
+          </div>
+        </li>
         {% if mailcow_locale %}
-        <li class="dropdown{% if available_languages|length == 1 %}lang-link-disabled{% endif %}">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="flag-icon flag-icon-{{ mailcow_locale }}"></span><span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
+        <li class="nav-item dropdown{% if available_languages|length == 1 %}lang-link-disabled{% endif %}">
+          <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false"><span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span></a>
+          <ul class="dropdown-menu" role="menu "aria-labelledby="languageDropdown">
             {% for key, val in available_languages %}
-            <li{% if mailcow_locale == key %} class="active"{% endif %}>
-              <a href="?{{ query_string({'lang': key}) }}">
-                <span class="flag-icon flag-icon-{{ key }}"></span>{{ val }}
+            <li>
+              <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
+                <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
               </a>
             </li>
             {% endfor %}
@@ -45,52 +58,60 @@
         </li>
         {% endif %}
         {% if mailcow_cc_role %}
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.mailcow_settings }} <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
+        <li class="nav-item dropdown">
+          <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.mailcow_system }}</a>
+          <ul class="dropdown-menu">
             {% if mailcow_cc_role == 'admin' %}
-            <li {% if is_uri('admin') %}class="active"{% endif %}><a href="/admin">{{ lang.header.administration }}</a></li>
-            <li {% if is_uri('debug') %}class="active"{% endif %}><a href="/debug">{{ lang.header.debug }}</a></li>
-            {% endif %}
-            {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
-            <li {% if is_uri('mailbox') %}class="active"{% endif %}><a href="/mailbox">{{ lang.header.mailboxes }}</a></li>
+            <li><a href="/debug" class="dropdown-item {% if is_uri('debug') %}active{% endif %}">{{ lang.header.debug }}</a></li>
+            <li><a href="/admin" class="dropdown-item {% if is_uri('admin') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
             {% endif %}
             {% if mailcow_cc_role != 'admin' %}
-            <li {% if is_uri('user') %}class="active"{% endif %}><a href="/user">{{ lang.header.user_settings }}</a></li>
+            <li><a href="/user" class="dropdown-item {% if is_uri('user') %}active{% endif %}">{{ lang.header.user_settings }}</a></li>
             {% endif %}
           </ul>
         </li>
-        <li {% if is_uri('quarantine') %}class="active"{% endif %}><a href="/quarantine"><i class="bi bi-inbox-fill"></i> {{ lang.header.quarantine }}</a></li>
-        {% endif %}
-        {% if mailcow_cc_role == 'admin' and not skip_sogo %}
-        <li><a href data-toggle="modal" data-container="sogo-mailcow" data-target="#RestartContainer"><i class="bi bi-arrow-repeat"></i> {{ lang.header.restart_sogo }}</a></li>
+        <li class="nav-item dropdown">
+          <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.email }}</a>
+          <ul class="dropdown-menu">
+            {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
+            <li><a href="/mailbox" class="dropdown-item {% if is_uri('mailbox') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
+            {% endif %}
+            <li><a href="/quarantine" class="dropdown-item {% if is_uri('quarantine') %}active{% endif %}">{{ lang.header.quarantine }}</a></li>
+            {% if mailcow_cc_role == 'admin' %}
+            <li><a href="/queue" class="dropdown-item {% if is_uri('queue') %}active{% endif %}">{{ lang.queue.queue_manager }}</a></li>
+            {% endif %}
+            {% if mailcow_cc_role == 'admin' %}
+            <li><a href="#" class="dropdown-item" data-bs-toggle="modal" data-container="sogo-mailcow" data-bs-target="#RestartContainer">{{ lang.header.restart_sogo }}</a></li>
+            {% endif %}
+          </ul>
+        </li>
         {% endif %}
         {% if mailcow_apps or app_links %}
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="bi bi-link-45deg"></i> {{ ui_texts.apps_name|raw }} <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
+        <li class="nav-item dropdown">
+          <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false"><i class="bi bi-link-45deg me-2"></i> {{ ui_texts.apps_name|raw }}</a>
+          <ul class="dropdown-menu">
             {% for app in mailcow_apps %}
               {% if not skip_sogo or not is_uri('SOGo', app.link) %}
               <li {% if app.description %}title="{{ app.description }}"{% endif %}>
-                <a href="{{ app.link }}">{{ app.name }}</a>
+                <a href="{{ app.link }}" class="dropdown-item">{{ app.name }}</a>
               </li>
               {% endif %}
             {% endfor %}
             {% for row in app_links %}
               {% for key, val in row %}
-              <li><a href="{{ val }}">{{ key }}</a></li>
+              <li><a href="{{ val }}" class="dropdown-item">{{ key }}</a></li>
               {% endfor %}
             {% endfor %}
           </ul>
         </li>
         {% endif %}
         {% if not dual_login and mailcow_cc_username %}
-        <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia">{{ mailcow_cc_username }}</b> <i class="bi bi-power"></i></a></li>
+        <li class="logged-in-as nav-item"><a href="#" onclick="logout.submit()" class="nav-link"><b class="username-lia">{{ mailcow_cc_username }}</b> <i class="bi bi-power ms-2"></i></a></li>
         {% elseif dual_login %}
-        <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia">{{ mailcow_cc_username }} <span class="text-info">({{ dual_login.username }})</span> </b><i class="bi bi-power"></i></a></li>
+        <li class="logged-in-as nav-item"><a href="#" onclick="logout.submit()" class="nav-link"><b class="username-lia">{{ mailcow_cc_username }} <span class="text-info">({{ dual_login.username }})</span> </b><i class="bi bi-power ms-2"></i></a></li>
         {% endif %}
         {% if not is_master %}
-        <li class="text-warning slave-info">[ slave ]</li>
+        <li class="text-warning slave-info nav-item">[ slave ]</li>
         {% endif %}
       </ul>
     </div><!--/.nav-collapse -->
@@ -101,12 +122,12 @@
 <form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
 
 {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri %}
-<div class="container">
+<div class="container mt-4">
   <div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div>
 </div>
 {% endif %}
 
-<div class="container">
+<div class="container my-4">
 {% block content %}{% endblock %}
 </div>
 
@@ -119,6 +140,17 @@
   var lang_tfa = {{ lang_tfa|raw }};
   var lang_fido2 = {{ lang_fido2|raw }};
   var docker_timeout = {{ docker_timeout|raw }} * 1000;
+  var mailcow_cc_role = '{{ mailcow_cc_role }}';
+  var last_login = '{{ last_login }}';
+  var mailcow_info = {
+    version_tag: '{{ mailcow_info.version_tag }}',
+    last_version_tag: '{{ mailcow_info.last_version_tag }}',
+    updatedAt: '{{ mailcow_info.updated_at }}',
+    project_url: '{{ mailcow_info.git_project_url }}',
+    project_owner: '{{ mailcow_info.git_owner }}',
+    project_repo: '{{ mailcow_info.git_repo }}',
+    branch: '{{ mailcow_info.mailcow_branch }}'
+  };
 
 $(window).scroll(function() {
   sessionStorage.scrollTop = $(this).scrollTop();
@@ -160,8 +192,8 @@
       }
     }
   }
-  }
-  $(window).load(function() {
+}
+  $(window).on('load', function() {
     $(".overlay").hide();
   });
   $(document).ready(function() {
@@ -172,43 +204,131 @@
     // TFA, CSRF, Alerts in footer.inc.php
     // Other general functions in mailcow.js
     {% for alert_type, alert_msg in alerts %}
-    mailcow_alert_box('{{ alert_msg|raw }}', '{{ alert_type }}');
+    mailcow_alert_box('{{ alert_msg|raw|e("js") }}', '{{ alert_type }}');
     {% endfor %}
 
     // Confirm TFA modal
-  {% if pending_tfa_method %}
-    $('#ConfirmTFAModal').modal({
+  {% if pending_tfa_methods %}
+    new bootstrap.Modal(document.getElementById("ConfirmTFAModal"), {
       backdrop: 'static',
       keyboard: false
-    });
-    $('#u2f_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_u2f + '</p>');
-    $('#ConfirmTFAModal').on('shown.bs.modal', function(){
-      $(this).find('input[name=token]').focus();
-      // If U2F
-      if(document.getElementById("u2f_auth_data") !== null) {
-        $.ajax({
-          type: "GET",
-          cache: false,
-          dataType: 'script',
-          url: "/api/v1/get/u2f-authentication/{{ pending_mailcow_cc_username|url_encode(true)|default('null') }}",
-          complete: function(data){
-            $('#u2f_status_auth').html(lang_tfa.waiting_usb_auth);
-            data;
-            setTimeout(function() {
-              console.log("Ready to authenticate");
-              u2f.sign(appId, challenge, registeredKeys, function(data) {
-                var form = document.getElementById('u2f_auth_form');
-                var auth = document.getElementById('u2f_auth_data');
-                console.log("Authenticate callback", data);
-                auth.value = JSON.stringify(data);
-                form.submit();
-              });
-            }, 1000);
-          }
-        });
+    }).show();
+
+
+    // validate Time based OTP tfa
+    $("#pending_tfa_tab_totp").click(function(){
+      $(".webauthn-authenticator-selection").removeClass("active");
+      $("#collapseWebAuthnTFA").collapse('hide');
+
+      // select default if only one authenticator exists
+      if ($('.totp-authenticator-selection').length == 1){
+        $('.totp-authenticator-selection').addClass("active");
+        var id = $('.totp-authenticator-selection').children('input').first().val();
+        $("#totp_selected_id").val(id);
+        $("#collapseTotpTFA").collapse('show');
       }
     });
+    $(".totp-authenticator-selection").click(function(){
+      $(".totp-authenticator-selection").removeClass("active");
+      $(this).addClass("active");
+      
+      var id = $(this).children('input').first().val();
+      $("#totp_selected_id").val(id);
+
+      $("#collapseTotpTFA").collapse('show');
+    });
+    if ($('.totp-authenticator-selection').length == 1 &&
+        $('#pending_tfa_tab_yubi_otp').length == 0 &&
+        $('.webauthn-authenticator-selection').length == 0){
+      
+      // select default if only one authenticator exists
+      $('.totp-authenticator-selection').addClass("active");
+
+      var id = $('.totp-authenticator-selection').children('input').first().val();
+      $("#totp_selected_id").val(id);
+
+      $("#collapseTotpTFA").collapse('show');
+      setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 1000);
+    }
+    $('#pending_tfa_tab_totp').on('shown.bs.tab', function() {
+      // autofocus
+      setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200);
+    });    
+    // validate Yubi OTP tfa
+    if ($('.webauthn-authenticator-selection').length == 0){
+      // autofocus
+      setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 1000);
+    }
+    $('#pending_tfa_tab_yubi_otp').on('shown.bs.tab', function() {
+      // autofocus
+      $("#collapseYubiTFA").find('input[name="token"]').focus();
+    });
+    // validate WebAuthn tfa
+    $("#pending_tfa_tab_webauthn").click(function(){
+      $(".totp-authenticator-selection").removeClass("active");
+
+      $("#collapseTotpTFA").collapse('hide');
+    });
+    $(".webauthn-authenticator-selection").click(function(){
+      $(".webauthn-authenticator-selection").removeClass("active");
+      $(this).addClass("active");
+      
+      var id = $(this).children('input').first().val();
+      $("#webauthn_selected_id").val(id);
+      
+      var webauthn_status_auth = document.getElementById('webauthn_status_auth');
+      webauthn_status_auth.style.setProperty('display', 'flex', 'important');
+      var webauthn_return_code = document.getElementById('webauthn_return_code');
+      webauthn_return_code.style.setProperty('display', 'none', 'important');
+
+      $("#collapseWebAuthnTFA").collapse('show');
+
+      $(this).find('input[name=token]').focus();
+      if(document.getElementById("webauthn_auth_data") !== null) {
+        // Check Browser support
+        if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
+            window.alert('Browser not supported for WebAuthn.');
+            return;
+        }
+
+        // fetch webauthn auth args
+        window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
+            return response.json();
+        }).then(json => {
+          console.log(json);
+          if (json.success === false) throw new Error();
+          if (json.type === "error") throw new Error(json.msg);
+      
+          recursiveBase64StrToArrayBuffer(json);
+          return json;
+        }).then(getCredentialArgs => {
+          // get credentials
+          return navigator.credentials.get(getCredentialArgs);
+        }).then(cred => {
+          return {
+            id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
+            clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+            authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
+            signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
+          };
+        }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
+          // send request by submit
+          var form = document.getElementById('webauthn_auth_form');
+          var auth = document.getElementById('webauthn_auth_data');
+          auth.value = AuthenticatorAttestationResponse;
+          form.submit();
+        }).catch(function(err) {
+          var webauthn_status_auth = document.getElementById('webauthn_status_auth');
+          webauthn_status_auth.style.setProperty('display', 'none', 'important');
+
+          var webauthn_return_code = document.getElementById('webauthn_return_code');
+          webauthn_return_code.style.setProperty('display', 'block', 'important');
+          webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
+        });
+      } 
+    });
     $('#ConfirmTFAModal').on('hidden.bs.modal', function(){
+      // cancel pending login
       $.ajax({
         type: "GET",
         cache: false,
@@ -219,7 +339,9 @@
         }
       });
     });
-    {% endif %}
+  {% endif %}
+
+
     // Validate FIDO2
   $("#fido2-login").click(function(){
     $('#fido2-alerts').html();
@@ -327,46 +449,60 @@
       });
       $("option:selected").prop("selected", false);
     }
-    if ($(this).val() == "u2f") {
-      $('#U2FModal').modal('show');
-      $("option:selected").prop("selected", false);
-      $("#start_u2f_register").click(function(){
-        $('#u2f_return_code').html('');
-        $('#u2f_return_code').hide();
-        $('#u2f_status_reg').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_u2f + '</p>');
-        $.ajax({
-          type: "GET",
-          cache: false,
-          dataType: 'script',
-          url: "/api/v1/get/u2f-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}",
-          complete: function(data){
-            data;
-            setTimeout(function() {
-              console.log("Ready to register");
-              $('#u2f_status_reg').html(lang_tfa.waiting_usb_register);
-              u2f.register(appId, registerRequests, registeredKeys, function(deviceResponse) {
-                var form  = document.getElementById('u2f_reg_form');
-                var reg   = document.getElementById('u2f_register_data');
-                console.log("Register callback: ", data);
-                if (deviceResponse.errorCode && deviceResponse.errorCode != 0) {
-                  var u2f_return_code = document.getElementById('u2f_return_code');
-                  u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
-                  if (deviceResponse.errorCode == "4") {
-                    deviceResponse.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle";
-                  }
-                  else if (deviceResponse.errorCode == "5") {
-                    deviceResponse.errorCode = "5 - Timeout reached before request could be satisfied.";
-                  }
-                  u2f_return_code.innerHTML = lang_tfa.error_code + ': ' + deviceResponse.errorCode + ' ' + lang_tfa.reload_retry;
-                  return;
+    if ($(this).val() == "webauthn") {
+        // check if Browser is supported
+        if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
+            window.alert('Browser not supported.');
+            return;
+        }
+
+        // show modal
+        $('#WebAuthnModal').modal('show');
+        $("option:selected").prop("selected", false);
+
+        $("#start_webauthn_register").click(() => {
+            var key_id = document.getElementsByName('key_id')[1].value;
+            var confirm_password = document.getElementsByName('confirm_password')[1].value;
+
+            // fetch WebAuthn create args
+            window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => {
+                return response.json();
+            }).then(json => {
+                console.log(json);
+                if (json.success === false) throw new Error(json.msg);
+                recursiveBase64StrToArrayBuffer(json);
+
+                return json;
+            }).then(createCredentialArgs => {
+                // create credentials
+                return navigator.credentials.create(createCredentialArgs);
+            }).then(cred => {
+                return {
+                    clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+                    attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
+                    key_id: key_id,
+                    tfa_method: "webauthn",
+                    confirm_password: confirm_password
+                };
+            }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
+                // send request
+                return window.fetch("/api/v1/add/webauthn-tfa-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
+            }).then(response => {
+                return response.json();
+            }).then(json => {
+                if (json.success) {
+                    // reload on success
+                    window.location = window.location.href.split("#")[0];
+                } else {
+                    throw new Error(json.msg);
                 }
-                reg.value = JSON.stringify(deviceResponse);
-                form.submit();
-              });
-            }, 1000);
-          }
+            }).catch(function(err) {
+                console.log(err);
+                var webauthn_return_code = document.getElementById('webauthn_return_code');
+                webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
+                webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
+            });
         });
-      });
     }
     if ($(this).val() == "none") {
       $('#DisableTFAModal').modal('show');
@@ -394,6 +530,21 @@
   {% if ui_texts.ui_footer %}
   <hr><span class="rot-enc">{{ ui_texts.ui_footer|rot13|raw }}</span>
   {% endif %}
+  {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "master" and mailcow_info.version_tag|default %}
+  <span class="version">
+    🐮 + 🐋 = 💕
+        Version: <a href="{{ mailcow_info.git_project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">{{ mailcow_info.version_tag }}
+    </a>
+  </span>
+  {% endif %}  
+  {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %}
+  <span class="version">
+    🛠️🐮 + 🐋 = 💕
+        Nightly: <a href="{{ mailcow_info.git_project_url }}/commit/{{ mailcow_info.git_commit }}" target="_blank">{{ mailcow_info.version_tag }}
+    </a><br>
+    <span style="text-align:right;display:block;">Build: {{ mailcow_info.git_commit_date }}</span>
+  </span>
+  {% endif %}
 </div>
 </body>
 </html>