123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- <!DOCTYPE html>
- <html lang="{{ mailcow_locale|default('en') }}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
- <meta name="theme-color" content="#F5D76E"/>
- <meta http-equiv="Referrer-Policy" content="same-origin">
- <title>{{ ui_texts.title_name|raw }}</title>
- <link rel="stylesheet" href="{{ css_path }}">
- <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>
- <div class="overlay"></div>
- {% block navbar %}
- <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top p-0">
- <div class="container-fluid">
- <a class="navbar-brand" href="/">
- <img class="main-logo" alt="mailcow-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}">
- <img class="main-logo-dark" alt="mailcow-logo-dark" src="{{ logo_dark|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="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="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>
- <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 %}
- </ul>
- </li>
- {% endif %}
- {% if mailcow_cc_role %}
- {% if mailcow_cc_role == 'admin' %}
- <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">
- <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>
- </ul>
- </li>
- {% endif %}
- {% if mailcow_cc_role != 'admin' %}
- <li class="nav-item dropdown">
- <a href="/user" class="nav-link" role="button" aria-expanded="false">{{ lang.header.user_settings }}</a>
- </li>
- {% endif %}
- {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
- <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">
- <li><a href="/mailbox" class="dropdown-item {% if is_uri('mailbox') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
- <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>
- <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_cc_role == 'user' %}
- <li class="nav-item dropdown">
- <a href="/quarantine" class="nav-link">{{ lang.header.quarantine }}</a>
- </li>
- {% endif %}
- {% endif %}
- {% if mailcow_apps_processed or app_links %}
- <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_processed %}
- {% if not skip_sogo or not is_uri('SOGo', app.user_link) %}
- <li {% if app.description %}title="{{ app.description }}"{% endif %}>
- <a href="{{ app.user_link }}" class="dropdown-item">{{ app.name }}</a>
- </li>
- {% endif %}
- {% endfor %}
- {% for row in app_links %}
- {% for key, val in row %}
- <li><a href="{{ val.user_link }}" class="dropdown-item">{{ key }}</a></li>
- {% endfor %}
- {% endfor %}
- </ul>
- </li>
- {% endif %}
- {% if not dual_login and mailcow_cc_username %}
- <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 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 %}
- <div class="nav-link form-check form-switch my-auto d-flex align-items-center">
- <li class="slave-info">[ slave ]</li>
- </div>
- {% endif %}
- </ul>
- </div><!--/.nav-collapse -->
- </div><!--/.container-fluid -->
- </nav>
- {% endblock navbar %}
- <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 mt-4">
- <div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div>
- </div>
- {% endif %}
- <div class="container flex-grow-1 my-4">
- {% block content %}{% endblock %}
- </div>
- {% include 'modals/footer.twig' %}
- <script src="{{ js_path }}"></script>
- <script>
- var lang_footer = {{ lang_footer|raw }};
- var lang_acl = {{ lang_acl|raw }};
- var lang_tfa = {{ lang_tfa|raw }};
- var lang_fido2 = {{ lang_fido2|raw }};
- var lang_success = {{ lang_success|raw }};
- var lang_danger = {{ lang_danger|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();
- });
- // Select language and reopen active URL without POST
- function setLang(sel) {
- $.post( '{{ uri }}', {lang: sel} );
- window.location.href = window.location.pathname + window.location.search;
- }
- // FIDO2 functions
- function arrayBufferToBase64(buffer) {
- let binary = '';
- let bytes = new Uint8Array(buffer);
- let len = bytes.byteLength;
- for (let i = 0; i < len; i++) {
- binary += String.fromCharCode( bytes[ i ] );
- }
- return window.btoa(binary);
- }
- function recursiveBase64StrToArrayBuffer(obj) {
- let prefix = '=?BINARY?B?';
- let suffix = '?=';
- if (typeof obj === 'object') {
- for (let key in obj) {
- if (typeof obj[key] === 'string') {
- let str = obj[key];
- if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
- str = str.substring(prefix.length, str.length - suffix.length);
- let binary_string = window.atob(str);
- let len = binary_string.length;
- let bytes = new Uint8Array(len);
- for (let i = 0; i < len; i++) {
- bytes[i] = binary_string.charCodeAt(i);
- }
- obj[key] = bytes.buffer;
- }
- } else {
- recursiveBase64StrToArrayBuffer(obj[key]);
- }
- }
- }
- }
- $(window).on('load', function() {
- $(".overlay").hide();
- });
- $(document).ready(function() {
- $(document).on('shown.bs.modal', function(e) {
- modal_id = $(e.relatedTarget).data('target');
- $(modal_id).attr("aria-hidden","false");
- });
- // 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|e("js") }}', '{{ alert_type }}');
- {% endfor %}
- // Confirm TFA modal
- {% if pending_tfa_methods %}
- new bootstrap.Modal(document.getElementById("ConfirmTFAModal"), {
- backdrop: 'static',
- keyboard: false
- }).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,
- dataType: 'script',
- url: '/inc/ajax/destroy_tfa_auth.php',
- complete: function(data){
- window.location = window.location.href.split("#")[0];
- }
- });
- });
- {% endif %}
- // Validate FIDO2
- $("#fido2-login").click(function(){
- $('#fido2-alerts').html();
- if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
- window.alert('Browser not supported.');
- return;
- }
- window.fetch("/api/v1/get/fido2-get-args", {method:'GET',cache:'no-cache'}).then(function(response) {
- return response.json();
- }).then(function(json) {
- if (json.success === false) {
- throw new Error();
- }
- recursiveBase64StrToArrayBuffer(json);
- return json;
- }).then(function(getCredentialArgs) {
- return navigator.credentials.get(getCredentialArgs);
- }).then(function(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) {
- return window.fetch("/api/v1/process/fido2-args", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
- }).then(function(response) {
- return response.json();
- }).then(function(json) {
- if (json.success) {
- window.location = window.location.href.split("#")[0];
- } else {
- throw new Error();
- }
- }).catch(function(err) {
- if (typeof err.message === 'undefined') {
- mailcow_alert_box(lang_fido2.fido2_validation_failed, "danger");
- } else {
- mailcow_alert_box(lang_fido2.fido2_validation_failed + ":<br><i>" + err.message + "</i>", "danger");
- }
- });
- });
- // Set TFA/FIDO2
- $("#register-fido2, #register-fido2-touchid").click(function(){
- let t = $(this);
- $("option:selected").prop("selected", false);
- if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
- window.alert('Browser not supported.');
- return;
- }
- window.fetch("/api/v1/get/fido2-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(function(response) {
- return response.json();
- }).then(function(json) {
- if (json.success === false) {
- throw new Error(json.msg);
- }
- recursiveBase64StrToArrayBuffer(json);
- // set attestation to node if we are registering apple touch id
- if(t.attr('id') === 'register-fido2-touchid') {
- json.publicKey.attestation = 'none';
- json.publicKey.authenticatorSelection.authenticatorAttachment = "platform";
- }
- return json;
- }).then(function(createCredentialArgs) {
- console.log(createCredentialArgs);
- return navigator.credentials.create(createCredentialArgs);
- }).then(function(cred) {
- return {
- clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
- attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
- };
- }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
- return window.fetch("/api/v1/add/fido2-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
- }).then(function(response) {
- return response.json();
- }).then(function(json) {
- if (json.success) {
- window.location = window.location.href.split("#")[0];
- } else {
- throw new Error(json.msg);
- }
- }).catch(function(err) {
- $('#fido2-alerts').html('<span class="text-danger"><b>' + err.message + '</b></span>');
- });
- });
- $('#selectTFA').change(function () {
- if ($(this).val() == "yubi_otp") {
- $('#YubiOTPModal').modal('show');
- $("option:selected").prop("selected", false);
- }
- if ($(this).val() == "totp") {
- $('#TOTPModal').modal('show');
- request_token = $('#tfa-qr-img').data('totp-secret');
- $.ajax({
- url: '/inc/ajax/qr_gen.php',
- data: {
- token: request_token,
- },
- }).done(function (result) {
- $("#tfa-qr-img").attr("src", result);
- });
- $("option:selected").prop("selected", false);
- }
- 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);
- }
- }).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');
- $("option:selected").prop("selected", false);
- }
- });
- {% if mailcow_cc_username %}
- // Reload after session timeout
- var session_lifetime = {{ (session_lifetime * 1000) + 15000 }};
- setTimeout(function() {
- location.reload();
- }, session_lifetime);
- {% endif %}
- // CSRF
- $('<input type="hidden" value="{{ csrf_token }}">').attr('name', 'csrf_token').appendTo('form');
- if (sessionStorage.scrollTop != "undefined") {
- $(window).scrollTop(sessionStorage.scrollTop);
- }
- });
- </script>
- <div class="container footer">
- {% 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>
|