diff --git a/.github/workflows/image_builds.yml b/.github/workflows/image_builds.yml
index 65678dff..496d4f73 100644
--- a/.github/workflows/image_builds.yml
+++ b/.github/workflows/image_builds.yml
@@ -28,7 +28,7 @@ jobs:
- "watchdog-mailcow"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup Docker
run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml
index 57aac781..e629e5e9 100644
--- a/.github/workflows/pr_to_nightly.yml
+++ b/.github/workflows/pr_to_nightly.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run the Action
diff --git a/.github/workflows/rebuild_backup_image.yml b/.github/workflows/rebuild_backup_image.yml
index 21c218a8..6c1d7e83 100644
--- a/.github/workflows/rebuild_backup_image.yml
+++ b/.github/workflows/rebuild_backup_image.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
diff --git a/.github/workflows/update_postscreen_access_list.yml b/.github/workflows/update_postscreen_access_list.yml
index 5d31eb9a..42502f30 100644
--- a/.github/workflows/update_postscreen_access_list.yml
+++ b/.github/workflows/update_postscreen_access_list.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Generate postscreen_access.cidr
run: |
diff --git a/data/Dockerfiles/dockerapi/main.py b/data/Dockerfiles/dockerapi/main.py
index 59d1a8ad..f9f02b63 100644
--- a/data/Dockerfiles/dockerapi/main.py
+++ b/data/Dockerfiles/dockerapi/main.py
@@ -198,8 +198,8 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub):
while True:
try:
- async with async_timeout.timeout(1):
- message = await channel.get_message(ignore_subscribe_messages=True)
+ async with async_timeout.timeout(60):
+ message = await channel.get_message(ignore_subscribe_messages=True, timeout=30)
if message is not None:
# Parse message
data_json = json.loads(message['data'].decode('utf-8'))
@@ -244,7 +244,7 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub):
else:
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
- await asyncio.sleep(0.01)
+ await asyncio.sleep(0.0)
except asyncio.TimeoutError:
pass
diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf
index 1b46d2b9..fb40de87 100644
--- a/data/conf/nginx/site.conf
+++ b/data/conf/nginx/site.conf
@@ -1,5 +1,6 @@
proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g;
-server_names_hash_bucket_size 64;
+server_names_hash_max_size 512;
+server_names_hash_bucket_size 128;
map $http_x_forwarded_proto $client_req_scheme {
default $scheme;
diff --git a/data/conf/nginx/templates/listen_ssl.template b/data/conf/nginx/templates/listen_ssl.template
index 93ec80c6..40c402d0 100644
--- a/data/conf/nginx/templates/listen_ssl.template
+++ b/data/conf/nginx/templates/listen_ssl.template
@@ -1,2 +1,3 @@
-listen ${HTTPS_PORT} ssl http2;
-listen [::]:${HTTPS_PORT} ssl http2;
+listen ${HTTPS_PORT} ssl;
+listen [::]:${HTTPS_PORT} ssl;
+http2 on;
diff --git a/data/web/admin.php b/data/web/admin.php
index d518abd6..aae50353 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -114,6 +114,7 @@ $template_data = [
'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps,
'logo_specs' => customize('get', 'main_logo_specs'),
+ 'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
'ip_check' => customize('get', 'ip_check'),
'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
diff --git a/data/web/inc/functions.customize.inc.php b/data/web/inc/functions.customize.inc.php
index 01460876..579185a2 100644
--- a/data/web/inc/functions.customize.inc.php
+++ b/data/web/inc/functions.customize.inc.php
@@ -24,9 +24,10 @@ function customize($_action, $_item, $_data = null) {
}
switch ($_item) {
case 'main_logo':
- if (in_array($_data['main_logo']['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
+ case 'main_logo_dark':
+ if (in_array($_data[$_item]['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
try {
- if (file_exists($_data['main_logo']['tmp_name']) !== true) {
+ if (file_exists($_data[$_item]['tmp_name']) !== true) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
@@ -34,7 +35,7 @@ function customize($_action, $_item, $_data = null) {
);
return false;
}
- $image = new Imagick($_data['main_logo']['tmp_name']);
+ $image = new Imagick($_data[$_item]['tmp_name']);
if ($image->valid() !== true) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -63,7 +64,7 @@ function customize($_action, $_item, $_data = null) {
return false;
}
try {
- $redis->Set('MAIN_LOGO', 'data:' . $_data['main_logo']['type'] . ';base64,' . base64_encode(file_get_contents($_data['main_logo']['tmp_name'])));
+ $redis->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name'])));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -207,8 +208,9 @@ function customize($_action, $_item, $_data = null) {
}
switch ($_item) {
case 'main_logo':
+ case 'main_logo_dark':
try {
- if ($redis->del('MAIN_LOGO')) {
+ if ($redis->del(strtoupper($_item))) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
@@ -260,8 +262,9 @@ function customize($_action, $_item, $_data = null) {
return $app_links;
break;
case 'main_logo':
+ case 'main_logo_dark':
try {
- return $redis->get('MAIN_LOGO');
+ return $redis->get(strtoupper($_item));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -298,9 +301,14 @@ function customize($_action, $_item, $_data = null) {
}
break;
case 'main_logo_specs':
+ case 'main_logo_dark_specs':
try {
$image = new Imagick();
- $img_data = explode('base64,', customize('get', 'main_logo'));
+ if($_item == 'main_logo_specs') {
+ $img_data = explode('base64,', customize('get', 'main_logo'));
+ } else {
+ $img_data = explode('base64,', customize('get', 'main_logo_dark'));
+ }
if ($img_data[1]) {
$image->readImageBlob(base64_decode($img_data[1]));
return $image->identifyImage();
diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php
index 5918cabe..3f80423e 100644
--- a/data/web/inc/header.inc.php
+++ b/data/web/inc/header.inc.php
@@ -66,6 +66,7 @@ $globalVariables = [
'ui_texts' => $UI_TEXTS,
'css_path' => '/cache/'.basename($CSSPath),
'logo' => customize('get', 'main_logo'),
+ 'logo_dark' => customize('get', 'main_logo_dark'),
'available_languages' => $AVAILABLE_LANGUAGES,
'lang' => $lang,
'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),
diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php
index 9e3c685d..c8333f97 100644
--- a/data/web/inc/triggers.inc.php
+++ b/data/web/inc/triggers.inc.php
@@ -147,10 +147,14 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
if (isset($_POST["submit_main_logo"])) {
if ($_FILES['main_logo']['error'] == 0) {
customize('add', 'main_logo', $_FILES);
+ }
+ if ($_FILES['main_logo_dark']['error'] == 0) {
+ customize('add', 'main_logo_dark', $_FILES);
}
}
if (isset($_POST["reset_main_logo"])) {
customize('delete', 'main_logo');
+ customize('delete', 'main_logo_dark');
}
// Some actions will not be available via API
if (isset($_POST["license_validate_now"])) {
diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php
index a77a3233..4d9d5da1 100644
--- a/data/web/inc/vars.inc.php
+++ b/data/web/inc/vars.inc.php
@@ -90,6 +90,7 @@ $AVAILABLE_LANGUAGES = array(
'es-es' => 'Español (Spanish)',
'fi-fi' => 'Suomi (Finish)',
'fr-fr' => 'Français (French)',
+ 'gr-gr' => 'Ελληνικά (Greek)',
'hu-hu' => 'Magyar (Hungarian)',
'it-it' => 'Italiano (Italian)',
'ko-kr' => '한국어 (Korean)',
@@ -99,6 +100,7 @@ $AVAILABLE_LANGUAGES = array(
'pt-pt' => 'Português (Portuguese)',
'ro-ro' => 'Română (Romanian)',
'ru-ru' => 'Pусский (Russian)',
+ 'si-si' => 'Slovenščina (Slovenian)',
'sk-sk' => 'Slovenčina (Slovak)',
'sv-se' => 'Svenska (Swedish)',
'tr-tr' => 'Türkçe (Turkish)',
diff --git a/data/web/js/build/013-mailcow.js b/data/web/js/build/013-mailcow.js
index 4cfc78e5..0f5d6f67 100644
--- a/data/web/js/build/013-mailcow.js
+++ b/data/web/js/build/013-mailcow.js
@@ -314,19 +314,28 @@ $(document).ready(function() {
$('#dark-mode-toggle').click(toggleDarkMode);
if ($('#dark-mode-theme').length) {
$('#dark-mode-toggle').prop('checked', true);
+ $('.main-logo').addClass('d-none');
+ $('.main-logo-dark').removeClass('d-none');
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
+ } else {
+ $('.main-logo').removeClass('d-none');
+ $('.main-logo-dark').addClass('d-none');
}
function toggleDarkMode(){
if($('#dark-mode-theme').length){
$('#dark-mode-theme').remove();
$('#dark-mode-toggle').prop('checked', false);
+ $('.main-logo').removeClass('d-none');
+ $('.main-logo-dark').addClass('d-none');
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png');
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
localStorage.setItem('theme', 'light');
}else{
$('head').append('');
$('#dark-mode-toggle').prop('checked', true);
+ $('.main-logo').addClass('d-none');
+ $('.main-logo-dark').removeClass('d-none');
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
localStorage.setItem('theme', 'dark');
diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js
index 497b923f..73d8befd 100644
--- a/data/web/js/site/mailbox.js
+++ b/data/web/js/site/mailbox.js
@@ -850,8 +850,9 @@ jQuery(function($){
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
- initComplete: function(){
+ initComplete: function(settings, json){
hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table');
+ filterByDomain(json, 8, table);
},
ajax: {
type: "GET",
@@ -1370,8 +1371,9 @@ jQuery(function($){
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
- initComplete: function(){
+ initComplete: function(settings, json){
hideTableExpandCollapseBtn('#tab-resources', '#resource_table');
+ filterByDomain(json, 5, table);
},
ajax: {
type: "GET",
@@ -1517,8 +1519,9 @@ jQuery(function($){
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[2, 'desc']],
- initComplete: function(){
+ initComplete: function(settings, json){
hideTableExpandCollapseBtn('#collapse-tab-bcc', '#bcc_table');
+ filterByDomain(json, 6, table);
},
ajax: {
type: "GET",
@@ -1831,8 +1834,9 @@ jQuery(function($){
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[2, 'desc']],
- initComplete: function(){
+ initComplete: function(settings, json){
hideTableExpandCollapseBtn('#tab-mbox-aliases', '#alias_table');
+ filterByDomain(json, 5, table);
},
ajax: {
type: "GET",
@@ -2337,6 +2341,40 @@ jQuery(function($){
else
$(tab).find(".table_collapse_option").hide();
}
+
+ function filterByDomain(json, column, table){
+ var tableId = $(table.table().container()).attr('id');
+ // Create the `select` element
+ var select = $('')
+ .insertBefore(
+ $('#'+tableId+' .dataTables_filter > label > input')
+ )
+ .on( 'change', function(){
+ table.column(column)
+ .search($(this).val())
+ .draw();
+ });
+
+ // get all domains
+ var domains = [];
+ json.forEach(obj => {
+ Object.entries(obj).forEach(([key, value]) => {
+ if(key === 'domain') {
+ domains.push(value)
+ }
+ });
+ });
+
+ // get unique domain list
+ domains = domains.filter(function(value, index, array) {
+ return array.indexOf(value) === index;
+ });
+
+ // add domains to select
+ domains.forEach(function(domain) {
+ select.append($(''));
+ });
+ }
// detect element visibility changes
function onVisible(element, callback) {
diff --git a/data/web/lang/lang.ca-es.json b/data/web/lang/lang.ca-es.json
index 26a30afd..877b46cd 100644
--- a/data/web/lang/lang.ca-es.json
+++ b/data/web/lang/lang.ca-es.json
@@ -3,7 +3,23 @@
"bcc_maps": "BCC maps",
"filters": "Filtres",
"recipient_maps": "Recipient maps",
- "syncjobs": "Feines de sincronització"
+ "syncjobs": "Feines de sincronització",
+ "quarantine_category": "Canvia la categoria de les notificacions de quarantena",
+ "quarantine_notification": "Canvia les notificacions de quarantena",
+ "sogo_profile_reset": "Restableix el prefil SOGo",
+ "alias_domains": "Afegir àlies de domini",
+ "app_passwds": "Gestiona les contrasenyes de les aplicacions",
+ "domain_desc": "Canvia la descripció del domini",
+ "eas_reset": "Restableix els dispositius EAS",
+ "login_as": "Inicia sessió com a usuari de la bústia de correu",
+ "prohibited": "Prohibit per ACL",
+ "protocol_access": "Canvia el protocol d'accés",
+ "quarantine": "Accions de quarantena",
+ "quarantine_attachments": "Fitxers adjunts en quarantena",
+ "spam_alias": "Àlies temporals",
+ "spam_score": "Puntuació de correu brossa",
+ "tls_policy": "Política TLS",
+ "unlimited_quota": "Quota ilimitada per bústies de correo"
},
"add": {
"activate_filter_warn": "All other filters will be deactivated, when active is checked.",
@@ -55,7 +71,9 @@
"target_domain": "Domini destí:",
"username": "Username",
"validate": "Validar",
- "validation_success": "Validated successfully"
+ "validation_success": "Validated successfully",
+ "app_name": "Nom de l'aplicació",
+ "app_password": "Afegir contrasenya a l'aplicació"
},
"admin": {
"access": "Accés",
@@ -259,7 +277,7 @@
},
"footer": {
"cancel": "Cancel·lar",
- "confirm_delete": "Confirma l'esborrat ",
+ "confirm_delete": "Confirma l'esborrat",
"delete_now": "Esborrar ara",
"delete_these_items": "Si et plau confirma els canvis al objecte amb id:",
"loading": "Si et plau espera ...",
diff --git a/data/web/lang/lang.cs-cz.json b/data/web/lang/lang.cs-cz.json
index d4f62495..b709de5e 100644
--- a/data/web/lang/lang.cs-cz.json
+++ b/data/web/lang/lang.cs-cz.json
@@ -459,6 +459,29 @@
"value_missing": "Prosím, uveďte všechny hodnoty",
"yotp_verification_failed": "Yubico OTP ověření selhalo: %s"
},
+ "datatables": {
+ "emptyTable": "Tabulka neobsahuje žádná data",
+ "info": "Zobrazuji _START_ až _END_ z celkem _TOTAL_ záznamů",
+ "infoEmpty": "Zobrazuji 0 až 0 z 0 záznamů",
+ "infoFiltered": "(filtrováno z celkem _MAX_ záznamů)",
+ "loadingRecords": "Načítám...",
+ "zeroRecords": "Žádné záznamy nebyly nalezeny",
+ "paginate": {
+ "first": "První",
+ "last": "Poslední",
+ "next": "Další",
+ "previous": "Předchozí"
+ },
+ "aria": {
+ "sortAscending": ": aktivujte pro seřazení vzestupně",
+ "sortDescending": ": aktivujte pro seřazení sestupně"
+ },
+ "lengthMenu": "Zobrazit _MENU_ výsledků",
+ "processing": "Zpracovávání...",
+ "search": "Vyhledávání:",
+ "decimal": ",",
+ "thousands": " "
+ },
"debug": {
"chart_this_server": "Graf (tento server)",
"containers_info": "Informace o kontejnerech",
diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json
index 82cbf172..706f0a0c 100644
--- a/data/web/lang/lang.de-de.json
+++ b/data/web/lang/lang.de-de.json
@@ -345,7 +345,9 @@
"api_read_only": "Schreibgeschützter Zugriff",
"api_read_write": "Lese-Schreib-Zugriff",
"oauth2_apps": "OAuth2 Apps",
- "queue_unban": "entsperren"
+ "queue_unban": "entsperren",
+ "allowed_methods": "Access-Control-Allow-Methods",
+ "allowed_origins": "Access-Control-Allow-Origin"
},
"danger": {
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json
index 5d898962..07881250 100644
--- a/data/web/lang/lang.en-gb.json
+++ b/data/web/lang/lang.en-gb.json
@@ -150,6 +150,8 @@
"ays": "Are you sure you want to proceed?",
"ban_list_info": "See a list of banned IPs below: network (remaining ban time) - [actions].
IPs queued to be unbanned will be removed from the active ban list within a few seconds.
Red labels indicate active permanent bans by blacklisting.",
"change_logo": "Change logo",
+ "logo_normal_label": "Normal",
+ "logo_dark_label": "Inverted for dark mode",
"configuration": "Configuration",
"convert_html_to_text": "Convert HTML to plain text",
"cors_settings": "CORS Settings",
diff --git a/data/web/lang/lang.es-es.json b/data/web/lang/lang.es-es.json
index e56e6bdd..78580ccc 100644
--- a/data/web/lang/lang.es-es.json
+++ b/data/web/lang/lang.es-es.json
@@ -20,7 +20,9 @@
"tls_policy": "Póliza de TLS",
"unlimited_quota": "Cuota ilimitada para buzones",
"app_passwds": "Gestionar las contraseñas de aplicaciones",
- "domain_desc": "Cambiar descripción del dominio"
+ "domain_desc": "Cambiar descripción del dominio",
+ "protocol_access": "Cambiar protocolo de acceso",
+ "quarantine_category": "Cambiar categoría de las notificaciones de cuarentena"
},
"add": {
"activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.",
@@ -85,7 +87,13 @@
"timeout2": "Tiempo de espera para la conexión al host local",
"username": "Usuario",
"validate": "Validar",
- "validation_success": "Validado exitosamente"
+ "validation_success": "Validado exitosamente",
+ "inactive": "Inactivo",
+ "app_name": "Nombre de la App",
+ "app_password": "Añadir contraseña para la app",
+ "public_comment": "Comentarios públicos",
+ "disable_login": "Desactivar login (el correo entrante seguirá activo)",
+ "comment_info": "Los comentarios privados no son visibles al usuario, mientras que los comentarios públicos aparecerán sobre la información general del usuario"
},
"admin": {
"access": "Acceso",
@@ -114,7 +122,7 @@
"app_name": "Nombre de la app",
"apps_name": "Nombre \"mailcow Apps\"",
"arrival_time": "Tiempo de llegada (hora del servidor)",
- "ban_list_info": "La lista de IPs bloqueadas sigue a continuación: red (tiempo de prohibición restante) - [acciones].
Las IPs en cola para ser desbloquadas se eliminarán de la lista de bloqueos en unos pocos segundos.
Las etiquetas rojas indican bloqueos permanentes permanentes mediante la inclusión en una lista negra.",
+ "ban_list_info": "Lista de IPs bloqueadas: red (tiempo de prohibición restante) - [acciones].
Las IPs en cola para ser desbloqueadas se eliminarán de la lista de bloqueos en unos pocos segundos.
Las etiquetas rojas indican bloqueos permanentes mediante la inclusión en la lista negra.",
"change_logo": "Cambiar logo",
"configuration": "Configuración",
"credentials_transport_warning": "Advertencia: al agregar una nueva entrada de ruta de transporte se actualizarán las credenciales para todas las entradas con una columna de \"siguiente destino\" coincidente.",
@@ -432,7 +440,7 @@
},
"header": {
"administration": "Administración",
- "debug": "Información del sistema",
+ "debug": "Información",
"email": "E-Mail",
"mailcow_config": "Configuración",
"quarantine": "Cuarentena",
diff --git a/data/web/lang/lang.fr-fr.json b/data/web/lang/lang.fr-fr.json
index 0bc0ba02..48420550 100644
--- a/data/web/lang/lang.fr-fr.json
+++ b/data/web/lang/lang.fr-fr.json
@@ -1096,7 +1096,8 @@
"weeks": "semaines",
"months": "mois",
"year": "année",
- "years": "années"
+ "years": "années",
+ "with_app_password": "avec le mot de passe de l'application"
},
"warning": {
"cannot_delete_self": "Impossible de supprimer l’utilisateur connecté",
diff --git a/data/web/lang/lang.gr-gr.json b/data/web/lang/lang.gr-gr.json
new file mode 100644
index 00000000..df9127ae
--- /dev/null
+++ b/data/web/lang/lang.gr-gr.json
@@ -0,0 +1,20 @@
+{
+ "user": {
+ "verify": "Επαλήθευση",
+ "week": "εβδομάδα",
+ "weekly": "Εβδομαδιαία",
+ "weeks": "Εβδομάδες",
+ "with_app_password": "με κωδικό εφαρμογής",
+ "year": "χρόνος",
+ "years": "χρόνια"
+ },
+ "warning": {
+ "cannot_delete_self": "Αδυναμία διαγραφής συνδεδεμένου χρήστη",
+ "dovecot_restart_failed": "Απέτυχε η επανεκκίνηση του Dovecot, παρακαλώ ελέγξτε τα αρχεία καταγραφής.",
+ "no_active_admin": "Αδυναμία απενεργοποίησης του τελευταίου ενεργού διαχειριστή",
+ "domain_added_sogo_failed": "Προστέθηκε το όνομα χώρου αλλά απέτυχε η επανεκκίνηση του SOGo.",
+ "hash_not_found": "Η κατακερματισμένη τιμή (hash value) δεν βρέθηκε ή έχει είδη διαγραφεί.",
+ "ip_invalid": "Παραλείφθηκε μη έγκυρη διεύθυνση IP: %s",
+ "is_not_primary_alias": "Παραλείφθηκε μη πρωτεύον ψευδώνυμο %s"
+ }
+}
diff --git a/data/web/lang/lang.hu-hu.json b/data/web/lang/lang.hu-hu.json
index f04cf9c5..327f0752 100644
--- a/data/web/lang/lang.hu-hu.json
+++ b/data/web/lang/lang.hu-hu.json
@@ -18,7 +18,11 @@
"transport_dest_format": "Szintaxis: pelda.hu, .pelda.hu, *, fiok@pelda.hu (több érték esetén vesszővel elválasztva)",
"upload": "Feltöltés",
"username": "Felhasználónév",
- "verify": "Ellenőrzés"
+ "verify": "Ellenőrzés",
+ "activate_api": "API aktiválása",
+ "activate_send": "Küldés gomb aktiválása",
+ "add": "Hozzáad",
+ "active": "Aktív"
},
"edit": {
"active": "Aktív",
@@ -385,9 +389,18 @@
"acl": {
"delimiter_action": "Elhatárolás",
"alias_domains": "Alias domainek hozzáadása",
- "app_passwds": "Alkalmazás jelszavak kezelése"
+ "app_passwds": "Alkalmazás jelszavak kezelése",
+ "domain_desc": "Domain leírás módosítása",
+ "filters": "Szűrők",
+ "login_as": "Bejelentkezés mint",
+ "quarantine": "Karantén műveletek",
+ "bcc_maps": "BCC címek"
},
"diagnostics": {
"dns_records": "DNS bejegyzések"
+ },
+ "add": {
+ "username": "Felhasználónév",
+ "validation_success": "Sikeres ellenőrzés"
}
}
diff --git a/data/web/lang/lang.it-it.json b/data/web/lang/lang.it-it.json
index 0d5b3f25..84f6e266 100644
--- a/data/web/lang/lang.it-it.json
+++ b/data/web/lang/lang.it-it.json
@@ -134,10 +134,10 @@
"admins_ldap": "Amministratori LDAP",
"advanced_settings": "Impostazioni avanzate",
"api_allow_from": "Allow API access from these IPs/CIDR network notations",
- "api_info": "The API is a work in progress. The documentation can be found at /api",
+ "api_info": "Questa API è in modifica. La documentazione può essere trovata su /api",
"api_key": "Chiave API",
"api_skip_ip_check": "Salta il controllo dell'IP per l'API",
- "app_links": "App links",
+ "app_links": "Link dell'app",
"app_name": "Nome dell'app",
"apps_name": "Nome \"mailcow Apps\"",
"arrival_time": "Ora di arrivo (ora del server)",
@@ -338,7 +338,8 @@
"oauth2_apps": "App OAuth2",
"oauth2_add_client": "Aggiungere il client OAuth2",
"rsettings_preset_4": "Disattivare Rspamd per un dominio",
- "options": "Opzioni"
+ "options": "Opzioni",
+ "cors_settings": "Impostazioni CORS"
},
"danger": {
"access_denied": "Accesso negato o form di login non corretto",
diff --git a/data/web/lang/lang.ro-ro.json b/data/web/lang/lang.ro-ro.json
index e6315db0..62585058 100644
--- a/data/web/lang/lang.ro-ro.json
+++ b/data/web/lang/lang.ro-ro.json
@@ -106,7 +106,8 @@
"timeout2": "Timeout pentru conectarea la gazda locală",
"username": "Nume de utilizator",
"validate": "Validează",
- "validation_success": "Validat cu succes"
+ "validation_success": "Validat cu succes",
+ "tags": "Etichete"
},
"admin": {
"access": "Acces",
@@ -334,7 +335,15 @@
"username": "Nume de utilizator",
"validate_license_now": "Validează GUID cu serverul de licență",
"verify": "Verifică",
- "yes": "✓"
+ "yes": "✓",
+ "cors_settings": "Setări CORS",
+ "f2b_ban_time_increment": "Timpul de blocare creşte cu fiecare blocare",
+ "f2b_max_ban_time": "Max. timp de blocare (s)",
+ "ip_check": "Verificaţie IP",
+ "ip_check_disabled": "Verificarea IP este dezactivată. Puteţi activa la
Sistem > Configuraţie > Opţiuni > Personalizează",
+ "ip_check_opt_in": "Alegeţi să folosiţi servicile ipv4.mailcow.email şi ipv6.mailcow.email să rezolvaţi addrese IP externale.",
+ "options": "Opţiuni",
+ "queue_unban": "retractează interzicere"
},
"danger": {
"access_denied": "Accesul a fost respins sau datele formularului sunt invalide",
@@ -453,7 +462,16 @@
"username_invalid": "Numele de utilizator %s nu poate fi utilizat",
"validity_missing": "Atribuie o perioadă de valabilitate",
"value_missing": "Furnizează toate valorile",
- "yotp_verification_failed": "Verificarea Yubico OTP a eșuat: %s"
+ "yotp_verification_failed": "Verificarea Yubico OTP a eșuat: %s",
+ "cors_invalid_method": "Aveţi specificaţi 'Allow-Method' invalid",
+ "webauthn_authenticator_failed": "Authentificator selectat nu a fost găsit",
+ "webauthn_publickey_failed": "Nici-o cheie publică a fost salvată pentru authenticatorul selectat",
+ "webauthn_username_failed": "Authenticatorul selectat aparţine la alt cont",
+ "demo_mode_enabled": "Mod de demonstraţie este activ",
+ "extended_sender_acl_denied": "lipseşte ACL pentru setarea adrese externe",
+ "template_exists": "Şablon %s deja există",
+ "template_id_invalid": "Şablon ID %s este invalid",
+ "template_name_invalid": "Nume de şablon este invalid"
},
"debug": {
"chart_this_server": "Grafic (acest server)",
@@ -641,7 +659,7 @@
"header": {
"administration": "Configurație și detalii",
"apps": "Aplicații",
- "debug": "Informații Sistem",
+ "debug": "Informaţie",
"email": "E-Mail",
"mailcow_config": "Configurație",
"quarantine": "Carantină",
@@ -1187,5 +1205,9 @@
"quota_exceeded_scope": "Cota de spațiu a domeniului depășită: Numai căsuțe poștale nelimitate pot fi create pe acest domeniu.",
"session_token": "Token formular invalid: Nepotrivire token",
"session_ua": "Token formular invalid: Eroare validare utilizator-agent"
+ },
+ "datatables": {
+ "expand_all": "Expandează tot",
+ "decimal": ","
}
}
diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json
index bad64184..dccaa204 100644
--- a/data/web/lang/lang.ru-ru.json
+++ b/data/web/lang/lang.ru-ru.json
@@ -338,7 +338,13 @@
"yes": "✓",
"queue_unban": "разблокировать",
"f2b_ban_time_increment": "Время бана увеличивается с каждым баном",
- "f2b_max_ban_time": "Максимальное время блокировки"
+ "f2b_max_ban_time": "Максимальное время блокировки",
+ "allowed_origins": "Access-Control-Allow-Origin",
+ "cors_settings": "Настройки CORS",
+ "allowed_methods": "Access-Control-Allow-Methods",
+ "ip_check": "Проверить IP",
+ "ip_check_disabled": "Проверка IP отключена. Вы можете включить его в разделе
Система > Конфигурация > Параметры > Настроить.",
+ "ip_check_opt_in": "Согласие на использование сторонних служб ipv4.mailcow.email и ipv6.mailcow.email для разрешения внешних IP-адресов."
},
"danger": {
"access_denied": "Доступ запрещён, или указаны неверные данные",
@@ -457,7 +463,10 @@
"username_invalid": "Имя пользователя %s нельзя использовать",
"validity_missing": "Пожалуйста, назначьте срок действия",
"value_missing": "Пожалуйста заполните все поля",
- "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s"
+ "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s",
+ "cors_invalid_method": "Указан недопустимый метод разрешения",
+ "demo_mode_enabled": "Демонстрационный режим включен",
+ "cors_invalid_origin": "Указан неверный Allow-Origin"
},
"debug": {
"chart_this_server": "Диаграмма (текущий сервер)",
@@ -542,7 +551,7 @@
"inactive": "Неактивный",
"kind": "Тип",
"last_modified": "Последние изменения",
- "lookup_mx": "Назначение на основе резовинга MX записи по регулярному выражению (.*\\.example\\.com$
для маршрутизации всей почты через этот хост, если MX заканчивающийся на example.com)",
+ "lookup_mx": "Назначение на основе резолвинга MX записи по регулярному выражению (.*\\.example\\.com$
для маршрутизации всей почты через этот хост, если MX заканчивающийся на example.com)",
"mailbox": "Изменение почтового аккаунта",
"mailbox_quota_def": "Квота по умолчанию",
"mailbox_relayhost_info": "Применяется только к почтовому ящику и личным псевдонимам, вне зависимости от настроек маршрутизации на уровне домена.",
@@ -607,7 +616,9 @@
"title": "Изменение объекта",
"unchanged_if_empty": "Если без изменений - оставьте пустым",
"username": "Имя пользователя",
- "validate_save": "Подтвердить и сохранить"
+ "validate_save": "Подтвердить и сохранить",
+ "sogo_access_info": "Единый вход из интерфейса почты продолжает работать. Эта настройка не влияет на доступ ко всем другим службам, а также не удаляет или изменяет существующий профиль пользователя SOGo.",
+ "app_passwd_protocols": "Разрешенные протоколы для пароля приложения"
},
"fido2": {
"confirm": "Подтвердить",
@@ -987,7 +998,8 @@
"verified_fido2_login": "Авторизация FIDO2 пройдена",
"verified_totp_login": "Авторизация TOTP пройдена",
"verified_webauthn_login": "Авторизация WebAuthn пройдена",
- "verified_yotp_login": "Авторизация Yubico OTP пройдена"
+ "verified_yotp_login": "Авторизация Yubico OTP пройдена",
+ "cors_headers_edited": "Настройки CORS сохранены"
},
"tfa": {
"api_register": "%s использует Yubico Cloud API. Пожалуйста, получите ключ API для вашего ключа здесь",
@@ -1169,7 +1181,12 @@
"weekly": "Раз в неделю",
"weeks": "недели",
"year": "год",
- "years": "лет"
+ "years": "лет",
+ "allowed_protocols": "Разрешенные протоколы",
+ "apple_connection_profile_with_app_password": "Новый пароль приложения генерируется и добавляется в профиль, поэтому при настройке устройства не требуется вводить пароль. Не предоставляйте доступ к файлу, поскольку он предоставляет полный доступ к вашему почтовому ящику.",
+ "direct_protocol_access": "Этот пользователь почтового ящика имеет прямой, внешний доступ к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.
Кнопка \"Вход в веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.",
+ "with_app_password": "с паролем приложения",
+ "change_password_hint_app_passwords": "В вашей учетной записи есть {{number_of_app_passwords}} паролей приложений, которые не будут изменены. Чтобы управлять ими, перейдите на вкладку \"Пароли приложений\"."
},
"warning": {
"cannot_delete_self": "Вы не можете удалить сами себя",
@@ -1183,5 +1200,8 @@
"quota_exceeded_scope": "Квота домена превышена: могут быть созданы только почтовые ящики без лимита.",
"session_token": "Неверный токен формы: несоответствие токена",
"session_ua": "Неверный токен формы: ошибка проверки User-Agent"
+ },
+ "datatables": {
+ "infoPostFix": ""
}
}
diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json
new file mode 100644
index 00000000..7c3ddbfd
--- /dev/null
+++ b/data/web/lang/lang.si-si.json
@@ -0,0 +1,396 @@
+{
+ "acl": {
+ "app_passwds": "Upravljaj gesla aplikacij",
+ "bcc_maps": "Preslikave SKP (BCC)",
+ "delimiter_action": "Dejanje ločila",
+ "domain_relayhost": "Spremeni gostitelja relay za domeno",
+ "eas_reset": "Ponastavi EAS naprave",
+ "filters": "Filtri",
+ "login_as": "Prijavi se kot uporabnik poštnega predala",
+ "mailbox_relayhost": "Spremeni gostitelja relay za poštni predal",
+ "prohibited": "Prepovedano z ACL",
+ "protocol_access": "Spremeni dostop do protokola",
+ "pushover": "Pushover",
+ "quarantine": "Dejanja karantene",
+ "quarantine_attachments": "Priponke v karanteno",
+ "quarantine_notification": "Spremeni obvestila o karanteni",
+ "ratelimit": "Omejitev pošiljanja",
+ "recipient_maps": "Preslikave prejemnikov",
+ "smtp_ip_access": "Spremeni dovoljene gostitelje za SMTP",
+ "sogo_access": "Dovoli upravljanje SOGo dostopov",
+ "sogo_profile_reset": "Ponastavi SOGo profil",
+ "spam_alias": "Začasni aliasi",
+ "spam_policy": "Blacklist/Whitelist",
+ "spam_score": "Ocena neželene pošte",
+ "tls_policy": "Politika TLS",
+ "unlimited_quota": "Neomejena kvota za poštne predale",
+ "alias_domains": "Dodaj alias domene",
+ "domain_desc": "Spremeni opis domene",
+ "extend_sender_acl": "Dovoli razširitev pošiljateljevega ACL z zunanjimi e-poštnimi naslovi",
+ "quarantine_category": "Spremeni kategorijo obvestil o karanteni",
+ "syncjobs": "Opravila sinhronizacije"
+ },
+ "add": {
+ "active": "Aktivno",
+ "add": "Dodaj",
+ "add_domain_only": "Dodaj samo domeno",
+ "add_domain_restart": "Dodaj domeno in ponovno zaženi SOGo",
+ "alias_address": "Alias naslov/i",
+ "alias_domain": "Alias domena",
+ "alias_domain_info": "Samo veljavne domene (ločene z vejico).",
+ "app_name": "Ime aplikacije",
+ "app_password": "Dodaj geslo aplikacije",
+ "app_passwd_protocols": "Dovoljeni protokoli za geslo aplikacije",
+ "automap": "Poskusi samodejno preslikati mape (\"Sent items\", \"Sent\" => \"Poslano\" ipd.)",
+ "backup_mx_options": "Možnosti posredovanja (relay)",
+ "comment_info": "Zasebni komentarji niso vidni uporabnikom, javni komentarji pa so prikazani kot tooltip, ko se z miško postavimo nad uporabnika v pregledu",
+ "custom_params": "Parametri po meri",
+ "custom_params_hint": "Pravilno: --param=xy, napačno: --param xy",
+ "delete1": "Izbriši na viru, ko je končano",
+ "delete2": "Izbriši sporočila na cilju, ki niso na viru",
+ "delete2duplicates": "Izbriši dvojnike na cilju",
+ "description": "Opis",
+ "destination": "Cilj",
+ "domain": "Domena",
+ "domain_matches_hostname": "Domena %s se ujema z nazivom gostitelja (hostname)",
+ "domain_quota_m": "Kvota za celotno domeno (MiB)",
+ "enc_method": "Metoda kriptiranja",
+ "exclude": "Izključi objekte (regex)",
+ "full_name": "Polno ime",
+ "gal": "Globalni seznam stikov (GAL)",
+ "generate": "generiraj",
+ "goto_ham": "Prepoznaj kot ham",
+ "goto_null": "Odstrani e-poštno sporočilo brez obvestila",
+ "goto_spam": "Prepoznaj kot spam",
+ "hostname": "Gostitelj",
+ "inactive": "Neaktivno",
+ "kind": "Tip",
+ "mailbox_quota_def": "Privzeta kvota za poštni predal",
+ "mailbox_username": "Uporabniško ime (levi del e-poštnega naslova)",
+ "max_aliases": "Največje število dovoljenih aliasov",
+ "max_mailboxes": "Največje dovoljeno število poštnih predalov",
+ "mins_interval": "Interval preverjanja (minute)",
+ "multiple_bookings": "Več rezervacij",
+ "nexthop": "Naslednji korak",
+ "password_repeat": "Potrditev gesla (ponovi)",
+ "port": "Vrata (port)",
+ "private_comment": "Zasebni komentar",
+ "public_comment": "Javni komentar",
+ "quota_mb": "Kvota (MiB)",
+ "relay_all": "Posreduj vse prejemnike (relay)",
+ "relay_all_info": "↪ Če izberete da ne posredujete vse prejemnike, morate ustvariti (\"slepi\") poštni predal za vsakega prejemnika, za katerega želite posredovati e-pošto.",
+ "relay_domain": "Posreduj to domeno (relay)",
+ "relay_unknown_only": "Posreduj samo neobstoječe poštne predale. V obstoječe poštne predale bo e-pošta dostavljena lokalno.",
+ "relayhost_wrapped_tls_info": "Prosim ne uporabljajte TLS-wrapped vrata (večinoma uporabljeno na vratih 465).
\nUporabite katera koli non-wrapped vrata in ustvarite STARTTLS. TLS politika za obvezno uporabo TLS se lahko ustvari pod \"Preslikave TLS politik\"",
+ "select": "Prosim izberite...",
+ "select_domain": "Prosim najprej izberite domeno",
+ "sieve_desc": "Kratek opis",
+ "sieve_type": "Vrsta filtra",
+ "skipcrossduplicates": "Preskoči podvojena sporočila po mapah (prvi pride, prvi melje)",
+ "subscribeall": "Prijavi vse mape",
+ "syncjob": "Dodaj opravilo sinhronizacije",
+ "tags": "Oznake",
+ "target_address": "Goto naslov",
+ "target_address_info": "Polni e-poštni naslov/i (ločeni z vejico).",
+ "target_domain": "Ciljna domena",
+ "timeout1": "Časovna omejitev za povezavo do oddaljenega gostitelja",
+ "username": "Uporabniško ime",
+ "validate": "Preveri",
+ "validation_success": "Uspešno preverjeno",
+ "activate_filter_warn": "Ko je aktivni izbran, bodo vsi ostali filtri deaktivirani.",
+ "alias_address_info": "Polni email naslov/i oziroma @example.com za zajem vseh sporočil domene (ločeno z vejico), samo domene mailcow.",
+ "bcc_dest_format": "BCC naslov mora biti en veljaven e-poštni naslov.
Če morate poslati kopijo na več naslov, ustvarite alias in ga uporabite tukaj.",
+ "disable_login": "Prepovej vpis (vhodna e-pošta je še vedno sprejeta)",
+ "gal_info": "GAL vsebuje vse objekte domene in ga uporabniki ne morejo urejati. Informacija o zasedenosti v SOGo ni na voljo, če je onemogočena! Ponovno zaženi SOGo za uveljavitev sprememb.",
+ "mailbox_quota_m": "Najvišja kvota na poštni predal (MiB)",
+ "password": "Geslo",
+ "post_domain_add": "SOGo container \"sogo-mailcow\" mora biti ponovno zagnan po dodajanju nove domene!
Dodatno se mora preveriti DNS konfiguracija domene. Ko je DNS konfiguracija domene odobrena, ponovno zaženite \"acme-mailcow\" za samodejno generiranje certifikatov za novo domeno (autoconfig.<domain>, autodiscover.<domain>).
Ta korak je opcijski in se ponovno poskuša vsakih 24 ur.",
+ "relay_transport_info": "
.*\\.google\\.com
za usmeritev vse pošte na MX, ki se konča z google.com, preko tega skoka)",
+ "main_name": "Naziv \"mailcow UI\"",
+ "merged_vars_hint": "Sive vrstice so združene iz vars.(local.)inc.php
in jih ni mogoče spremeniti.",
+ "oauth2_info": "OAuth2 implementacija omogoča grant vrste \"Authorization code\" in izdaja refresh tokene./oauth/authorize
/oauth/token
/oauth/profile
inc/vars.local.inc.php
da jih lahko vklopite ali izklopite.",
+ "relayhosts_hint": "Določite transporte glede na pošiljatelja, da jih lahko izberete v konfiguraciji domene./.+@domain\\.tld/i
).Журнали контейнерів mailcow зберігаються в Redis, і раз на хвилину рядки журналу за межами LOG_LINES (%d)
видаляються, щоб зменшити навантаження на сервер.\n
Самі журнали контейнерів не зберігаються після перезавантаження контейнера. Усі контейнери додатково пишуть логи у службу Docker, і, отже, використовують драйвер логування за промовчанням. Журнали контейнерів призначені лише для налагодження дрібних проблем. Для інших завдань, будь ласка, настройте драйвер логування Docker самостійно.
Зовнішні журнали збираються через API програм.
\nСтатичні журнали – це в основному журнали активності, які не записуються в Dockerd, але все одно повинні бути постійними (за винятком журналів API).
" + "log_info": "Журнали контейнерів mailcow зберігаються в Redis, і раз на хвилину рядки журналу за межами LOG_LINES (%d)
видаляються, щоб зменшити навантаження на сервер.\n
Самі журнали контейнерів не зберігаються після перезавантаження контейнера. Усі контейнери додатково пишуть логи у службу Docker, і, отже, використовують драйвер логування за промовчанням. Журнали контейнерів призначені лише для налагодження дрібних проблем. Для інших завдань, будь ласка, настройте драйвер логування Docker самостійно.
Зовнішні журнали збираються через API програм.
\nСтатичні журнали – це в основному журнали активності, які не записуються в Dockerd, але все одно повинні бути постійними (за винятком журналів API).
", + "error_show_ip": "Не вдалося розпізнати публічні IP-адреси", + "no_update_available": "Система працює на останній версії", + "architecture": "Архітектура", + "container_running": "Працює", + "container_disabled": "Контейнер зупинено або вимкнено", + "container_stopped": "Зупинено", + "cores": "Ядра", + "current_time": "Системний час", + "memory": "Пам'ять", + "show_ip": "Показати загальнодоступну IP-адресу", + "timezone": "Часовий пояс", + "update_available": "Доступне оновлення", + "update_failed": "Не вдалося перевірити наявність оновлень", + "wip": "Наразі робота триває" }, "diagnostics": { "cname_from_a": "Значення, отримане із запису A/AAAA. Це підтримується, поки запис вказує на правильний ресурс.", @@ -607,7 +641,8 @@ "sender_acl_info": "Врахуйте, що якщо користувачеві поштового облікового запису А дозволено відправляти від імені користувача Б, то адреса користувача Б не з'явиться автоматично у списку \"Відправник\" при написанні листів у SOGo.smtp_tls_mandatory_protocols
та smtp_tls_mandatory_ciphers
."
+ "tls_policy_maps_enforced_tls": "Для вихідних повідомлень від користувачів із включеною примусовою політикою шифрування вихідних з'єднань не описані глобальною політикою, будуть застосовані значення за замовчуванням, зазначені в smtp_tls_mandatory_protocols
та smtp_tls_mandatory_ciphers
.",
+ "add_template": "Додати шаблон",
+ "domain_templates": "Шаблони доменів",
+ "relay_unknown": "Ретрансляція невідомих поштових скриньок",
+ "mailbox_templates": "Шаблони поштових скриньок",
+ "templates": "Шаблони",
+ "template": "Шаблон"
},
"oauth2": {
"authorize_app": "Авторизація додатка",
@@ -896,7 +938,20 @@
"table_size_show_n": "Відображати %s полів"
},
"queue": {
- "queue_manager": "Черга на відправлення"
+ "queue_manager": "Черга на відправлення",
+ "delete": "Видалити все",
+ "info": "Поштова черга містить усі електронні листи, які очікують на доставку. Якщо електронний лист застряг у черзі на тривалий час, він автоматично видаляється системою.{{ lang.admin.logo_info }}
- {% if logo %} -