From 4c495200c8fba3b0384d896f47c8b7af3edc4956 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 24 Apr 2023 12:54:56 +0200 Subject: [PATCH] Customize SOGo theme from Web UI --- data/Dockerfiles/dockerapi/dockerapi.py | 45 +++- data/Dockerfiles/sogo/Dockerfile | 7 +- data/Dockerfiles/sogo/bootstrap-sogo.sh | 2 + data/Dockerfiles/sogo/customize.sh | 112 ++++++++++ data/conf/sogo/cow_mailcow.svg | 182 ++++++++++++++++ data/conf/sogo/custom-palettes.js | 7 + data/conf/sogo/custom-theme.js | 46 ++-- data/web/admin.php | 3 + data/web/api/openapi.yaml | 105 +++++++++ data/web/inc/functions.customize.inc.php | 203 ++++++++++++++++++ data/web/inc/header.inc.php | 1 + data/web/inc/triggers.inc.php | 8 + data/web/inc/vars.inc.php | 24 +++ data/web/json_api.php | 30 +-- data/web/lang/lang.de-de.json | 7 + data/web/lang/lang.en-gb.json | 10 + .../templates/admin/tab-config-customize.twig | 88 ++++++-- data/web/templates/base.twig | 4 +- docker-compose.yml | 4 +- 19 files changed, 831 insertions(+), 57 deletions(-) create mode 100644 data/Dockerfiles/sogo/customize.sh create mode 100644 data/conf/sogo/cow_mailcow.svg create mode 100644 data/conf/sogo/custom-palettes.js diff --git a/data/Dockerfiles/dockerapi/dockerapi.py b/data/Dockerfiles/dockerapi/dockerapi.py index 1ab651b5..28a611e7 100644 --- a/data/Dockerfiles/dockerapi/dockerapi.py +++ b/data/Dockerfiles/dockerapi/dockerapi.py @@ -273,7 +273,50 @@ class DockerUtils: # todo: check each exit code res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} return Response(content=json.dumps(res, indent=4), media_type="application/json") - + + # api call: container_post - post_action: exec - cmd: sogo - task: customize_enable + def container_post__exec__sogo__customize_enable(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/customize.sh enable"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: sogo - task: customize_disable + def container_post__exec__sogo__customize_disable(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/customize.sh disable"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: sogo - task: set_logo + def container_post__exec__sogo__set_logo(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/customize.sh set_logo"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: sogo - task: remove_logo + def container_post__exec__sogo__remove_logo(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "rm -f /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: sogo - task: set_favicon + def container_post__exec__sogo__set_favicon(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/customize.sh set_favicon"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: sogo - task: remove_favicon + def container_post__exec__sogo__remove_favicon(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "cp /sogo.ico /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: sogo - task: set_theme + def container_post__exec__sogo__set_theme(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/customize.sh set_theme"] + sogo_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sogo_return) + # api call: container_post - post_action: exec - cmd: mailq - task: list def container_post__exec__mailq__list(self, container_id, request_json): for container in self.docker_client.containers.list(filters={"id": container_id}): diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index da8f23be..5cf10454 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -25,6 +25,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ psmisc \ wget \ patch \ + redis-tools \ && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ && chmod +x /usr/local/bin/gosu \ @@ -46,10 +47,14 @@ COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY supervisord.conf /etc/supervisor/supervisord.conf COPY acl.diff /acl.diff COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh +COPY customize.sh / COPY docker-entrypoint.sh / +RUN rm -rf /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg +RUN mv /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico /sogo.ico RUN chmod +x /bootstrap-sogo.sh \ - /usr/local/sbin/stop-supervisor.sh + /usr/local/sbin/stop-supervisor.sh \ + /customize.sh ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh index aa15525c..e0e08a95 100755 --- a/data/Dockerfiles/sogo/bootstrap-sogo.sh +++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -240,6 +240,8 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist # Copy logo, if any [[ -f /etc/sogo/sogo-full.svg ]] && cp /etc/sogo/sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg +# Use the mailcow logo if no sogo-full.svg file does exist +! [[ -f /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg ]] && cp /etc/sogo/cow_mailcow.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg # Rsync web content echo "Syncing web content with named volume" diff --git a/data/Dockerfiles/sogo/customize.sh b/data/Dockerfiles/sogo/customize.sh new file mode 100644 index 00000000..8d65f1ec --- /dev/null +++ b/data/Dockerfiles/sogo/customize.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +if [[ "$1" == "enable" ]]; then + # enable debug mode + if grep -q "SOGoUIxDebugEnabled = YES;" "/etc/sogo/sogo.conf"; then + sed -i "s|//SOGoUIxDebugEnabled = YES;|SOGoUIxDebugEnabled = YES;|" "/etc/sogo/sogo.conf" + else + echo "SOGoUIxDebugEnabled = YES;" >> "/etc/sogo/sogo.conf" + fi + + echo "Success: SOGoUIxDebugEnabled has been enabled" +elif [[ "$1" == "disable" ]]; then + # disable debug mode + if grep -q "SOGoUIxDebugEnabled = YES;" "/etc/sogo/sogo.conf"; then + if ! grep -q "//SOGoUIxDebugEnabled = YES;" "/etc/sogo/sogo.conf"; then + sed -i "s|SOGoUIxDebugEnabled = YES;|//SOGoUIxDebugEnabled = YES;|" "/etc/sogo/sogo.conf" + fi + fi + + echo "Success: SOGoUIxDebugEnabled has been disabled" +elif [[ "$1" == "set_theme" ]]; then + # Get the sogo palettes from Redis + PRIMARY=$(redis-cli -h redis HGET SOGO_THEME primary) + if [ $? -ne 0 ]; then + PRIMARY="green" + fi + ACCENT=$(redis-cli -h redis HGET SOGO_THEME accent) + if [ $? -ne 0 ]; then + ACCENT="green" + fi + BACKGROUND=$(redis-cli -h redis HGET SOGO_THEME background) + if [ $? -ne 0 ]; then + BACKGROUND="grey" + fi + + # Read custom palettes + if [ -f /etc/sogo/custom-palettes.js ]; then + COLORS=$(cat /etc/sogo/custom-palettes.js) + else + COLORS="" + fi + + # Write theme to /usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js + cat > /usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js < /tmp/logo_base64.txt + + # Check if mime type is svg+xml + mime_type=$(awk -F'[:;]' '{print $2}' /tmp/logo_base64.txt | sed 's/.*\///') + if [ "$mime_type" != "svg+xml" ]; then + echo "Error: Image format must be of type svg" + exit 1 + fi + + # Decode base64 and save to file + payload=$(cat /tmp/logo_base64.txt | sed 's/^data:[^;]*;//' | awk '{ sub(/^base64,/, ""); print $0 }') + echo $payload | base64 -d | tee /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg > /dev/null + + # Remove temp file + rm /tmp/logo_base64.txt + echo "Success: Image has been set" +elif [[ "$1" == "set_favicon" ]]; then + # Get the image data from Redis and save it to a tmp file + redis-cli -h redis GET FAVICON > /tmp/favicon_base64.txt + + # Check if mime type is png or ico + mime_type=$(awk -F'[:;]' '{print $2}' /tmp/favicon_base64.txt | sed 's/.*\///') + if [[ "$mime_type" != "png" && "$mime_type" != "ico" ]]; then + echo "Error: Image format must be of type png or ico" + exit 1 + fi + + # Decode base64 and save to file + payload=$(cat /tmp/favicon_base64.txt | sed 's/^data:[^;]*;//' | awk '{ sub(/^base64,/, ""); print $0 }') + echo $payload | base64 -d | tee /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico > /dev/null + + # Remove temp file + rm /tmp/favicon_base64.txt + echo "Success: Image has been set" +fi \ No newline at end of file diff --git a/data/conf/sogo/cow_mailcow.svg b/data/conf/sogo/cow_mailcow.svg new file mode 100644 index 00000000..6ba98e46 --- /dev/null +++ b/data/conf/sogo/cow_mailcow.svg @@ -0,0 +1,182 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/data/conf/sogo/custom-palettes.js b/data/conf/sogo/custom-palettes.js new file mode 100644 index 00000000..1afa1e6f --- /dev/null +++ b/data/conf/sogo/custom-palettes.js @@ -0,0 +1,7 @@ +$mdThemingProvider.definePalette("sogo-green",{50:"eaf5e9",100:"cbe5c8",200:"aad6a5",300:"88c781",400:"66b86a",500:"56b04c",600:"4da143",700:"388e3c",800:"367d2e",900:"225e1b",A100:"ffffff",A200:"69f0ae",A400:"00e676",A700:"00c853",contrastDefaultColor:"dark",contrastLightColors:["300","400","500","600","700","800","900"]}) +$mdThemingProvider.definePalette("sogo-blue",{50:"f0faf9",100:"e1f5f3",200:"ceebe8",300:"bfe0dd",400:"b2d6d3",500:"a1ccc8",600:"8ebfbb",700:"7db3b0",800:"639997",900:"4d8080",A100:"d4f7fa",A200:"c3f5fa",A400:"53e3f0",A700:"00b0c0",contrastDefaultColor:"light",contrastDarkColors:["50","100","200"]}) +$mdThemingProvider.definePalette("sogo-grey",$mdThemingProvider.extendPalette("grey",{1e3:"baa870"})) + +var primarySettings = {default:"900","hue-1":"400","hue-2":"800","hue-3":"A700"} +var accentSettings = {default:"500","hue-1":"A100","hue-2":"300","hue-3":"A700"} +var backgroundSettings = {} diff --git a/data/conf/sogo/custom-theme.js b/data/conf/sogo/custom-theme.js index 0df50677..5ad83a94 100644 --- a/data/conf/sogo/custom-theme.js +++ b/data/conf/sogo/custom-theme.js @@ -1,36 +1,34 @@ -/* EXAMPLE - EXAMPLE - EXAMPLE - EXAMPLE - EXAMPLE - EXAMPLE - EXAMPLE (function() { 'use strict'; + angular.module('SOGo.Common') .config(configure) configure.$inject = ['$mdThemingProvider']; function configure($mdThemingProvider) { - var greyMap = $mdThemingProvider.extendPalette('grey', { - '200': 'F5F5F5', - '300': 'E5E5E5', - '1000': '4C566A' + +$mdThemingProvider.definePalette("sogo-green",{50:"eaf5e9",100:"cbe5c8",200:"aad6a5",300:"88c781",400:"66b86a",500:"56b04c",600:"4da143",700:"388e3c",800:"367d2e",900:"225e1b",A100:"ffffff",A200:"69f0ae",A400:"00e676",A700:"00c853",contrastDefaultColor:"dark",contrastLightColors:["300","400","500","600","700","800","900"]}) +$mdThemingProvider.definePalette("sogo-blue",{50:"f0faf9",100:"e1f5f3",200:"ceebe8",300:"bfe0dd",400:"b2d6d3",500:"a1ccc8",600:"8ebfbb",700:"7db3b0",800:"639997",900:"4d8080",A100:"d4f7fa",A200:"c3f5fa",A400:"53e3f0",A700:"00b0c0",contrastDefaultColor:"light",contrastDarkColors:["50","100","200"]}) +$mdThemingProvider.definePalette("sogo-grey",$mdThemingProvider.extendPalette("grey",{1e3:"baa870"})) + +var primarySettings = {default:"900","hue-1":"400","hue-2":"800","hue-3":"A700"} +var accentSettings = {default:"500","hue-1":"A100","hue-2":"300","hue-3":"A700"} +var backgroundSettings = {} + + var primary = $mdThemingProvider.extendPalette('sogo-blue', {}); + var accent = $mdThemingProvider.extendPalette('sogo-green', { + 'A100': 'ffffff' }); - var greenCow = $mdThemingProvider.extendPalette('green', { - '600': 'E5E5E5' - }); - $mdThemingProvider.definePalette('frost-grey', greyMap); - $mdThemingProvider.definePalette('green-cow', greenCow); + var background = $mdThemingProvider.extendPalette('sogo-grey', {}); + + $mdThemingProvider.definePalette('primary-cow', primary); + $mdThemingProvider.definePalette('accent-cow', accent); + $mdThemingProvider.definePalette('background-cow', background); + $mdThemingProvider.theme('default') - .primaryPalette('green-cow', { - 'default': '400', - 'hue-1': '400', - 'hue-2': '600', - 'hue-3': 'A700' - }) - .accentPalette('green', { - 'default': '600', - 'hue-1': '300', - 'hue-2': '300', - 'hue-3': 'A700' - }) - .backgroundPalette('frost-grey'); + .primaryPalette('primary-cow', primarySettings) + .accentPalette('accent-cow', accentSettings) + .backgroundPalette('background-cow', backgroundSettings); $mdThemingProvider.generateThemesOnDemand(false); } })(); - */ \ No newline at end of file diff --git a/data/web/admin.php b/data/web/admin.php index cd3eb890..97709254 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -103,9 +103,12 @@ $template_data = [ 'rsettings' => $rsettings, 'rspamd_regex_maps' => $rspamd_regex_maps, 'logo_specs' => customize('get', 'main_logo_specs'), + 'favicon_specs' => customize('get', 'favicon_specs'), 'ip_check' => customize('get', 'ip_check'), 'password_complexity' => password_complexity('get'), 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], + 'sogo_palettes' => $GLOBALS['SOGO_PALETTES'], + 'sogo_theme' => customize('get', 'sogo_theme'), 'lang_admin' => json_encode($lang['admin']), 'lang_datatables' => json_encode($lang['datatables']) ]; diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 65bd1211..ae833abe 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -5602,6 +5602,109 @@ paths: description: You can list all mailboxes existing in system for a specific domain. operationId: Get mailboxes of a domain summary: Get mailboxes of a domain + /api/v1/edit/sogo_theme/: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + log: + - customize + - edit + - sogo_theme + - primary: "brown" + accent: "brown" + background: "amber" + msg: + - sogo_theme_modified + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Customize + description: >- + Using this endpoint you can edit the sogo theme. SOGo has to be restarted after each change. + operationId: Edit SOGo theme + requestBody: + content: + application/json: + schema: + example: + primary: "brown" + accent: "brown" + background: "amber" + summary: Edit SOGo theme + /api/v1/delete/sogo_theme/: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + log: + - customize + - delete + - sogo_theme + - items: + - sogo-theme + msg: "sogo_theme_removed" + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Customize + description: >- + Using this endpoint you can reset the sogo theme. SOGo has to be restarted after each change. + operationId: Reset SOGo theme + requestBody: + content: + application/json: + schema: + example: + - items: + - "sogo-theme" + summary: Reset SOGo theme tags: - name: Domains @@ -5646,3 +5749,5 @@ tags: description: Get the status of your cow - name: Ratelimits description: Edit domain ratelimits + - name: Customize + description: You can customize mailcow's appearance \ No newline at end of file diff --git a/data/web/inc/functions.customize.inc.php b/data/web/inc/functions.customize.inc.php index 6025d97d..8635c7f0 100644 --- a/data/web/inc/functions.customize.inc.php +++ b/data/web/inc/functions.customize.inc.php @@ -73,6 +73,81 @@ function customize($_action, $_item, $_data = null) { ); return false; } + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_logo')); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'upload_success' + ); + break; + case 'favicon': + if (in_array($_data['favicon']['type'], array('image/png', 'image/x-icon'))) { + try { + if (file_exists($_data['favicon']['tmp_name']) !== true) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'img_tmp_missing' + ); + return false; + } + $image = new Imagick($_data['favicon']['tmp_name']); + if ($image->valid() !== true) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'img_invalid' + ); + return false; + } + $available_sizes = array(32, 128, 180, 192, 256); + if ($image->getImageWidth() != $image->getImageHeight()) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'img_invalid' + ); + return false; + } + if (!in_array($image->getImageWidth(), $available_sizes)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'img_invalid' + ); + return false; + } + $image->destroy(); + } + catch (ImagickException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'img_invalid' + ); + return false; + } + } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'invalid_mime_type' + ); + return false; + } + try { + $redis->Set('FAVICON', 'data:' . $_data['favicon']['type'] . ';base64,' . base64_encode(file_get_contents($_data['favicon']['tmp_name']))); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_favicon')); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_item, $_data), @@ -179,6 +254,34 @@ function customize($_action, $_item, $_data = null) { 'msg' => 'ip_check_opt_in_modified' ); break; + case 'sogo_theme': + $_data['primary'] = (isset($_data['primary']) && in_array($_data['primary'], $GLOBALS['SOGO_PALETTES'])) ? $_data['primary'] : 'green'; + $_data['accent'] = (isset($_data['accent']) && in_array($_data['accent'], $GLOBALS['SOGO_PALETTES'])) ? $_data['accent'] : 'green'; + $_data['background'] = (isset($_data['background']) && in_array($_data['background'], $GLOBALS['SOGO_PALETTES'])) ? $_data['background'] : 'grey'; + + try { + $redis->hSet('SOGO_THEME', 'primary', $_data['primary']); + $redis->hSet('SOGO_THEME', 'accent', $_data['accent']); + $redis->hSet('SOGO_THEME', 'background', $_data['background']); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'customize_enable')); + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_theme')); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'sogo_theme_modified' + ); + return true; + break; } break; case 'delete': @@ -203,6 +306,7 @@ function customize($_action, $_item, $_data = null) { case 'main_logo': try { if ($redis->del('MAIN_LOGO')) { + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'remove_logo')); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_item, $_data), @@ -219,6 +323,51 @@ function customize($_action, $_item, $_data = null) { ); return false; } + break; + case 'favicon': + try { + if ($redis->del('FAVICON')) { + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'remove_favicon')); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'reset_favicon' + ); + return true; + } + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + break; + case 'sogo_theme': + try { + $redis->hSet('SOGO_THEME', 'primary', 'sogo-blue'); + $redis->hSet('SOGO_THEME', 'accent', 'sogo-green'); + $redis->hSet('SOGO_THEME', 'background', 'sogo-grey'); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_theme')); + $docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'customize_disable')); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'sogo_theme_removed' + ); + return true; break; } break; @@ -251,6 +400,19 @@ function customize($_action, $_item, $_data = null) { return false; } break; + case 'favicon': + try { + return $redis->get('FAVICON'); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + break; case 'ui_texts': try { $data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI'; @@ -295,6 +457,25 @@ function customize($_action, $_item, $_data = null) { return false; } break; + case 'favicon_specs': + try { + $image = new Imagick(); + $img_data = explode('base64,', customize('get', 'favicon')); + if ($img_data[1]) { + $image->readImageBlob(base64_decode($img_data[1])); + return $image->identifyImage(); + } + return false; + } + catch (ImagickException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'imagick_exception' + ); + return false; + } + break; case 'ip_check': try { $ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0; @@ -309,6 +490,28 @@ function customize($_action, $_item, $_data = null) { return false; } break; + case 'sogo_theme': + $data = array(); + try { + $data['primary'] = $redis->hGet('SOGO_THEME', 'primary'); + $data['accent'] = $redis->hGet('SOGO_THEME', 'accent'); + $data['background'] = $redis->hGet('SOGO_THEME', 'background'); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + + $data['primary'] = empty($data['primary']) ? 'sogo-blue' : $data['primary']; + $data['accent'] = empty($data['accent']) ? 'sogo-green' : $data['accent']; + $data['background'] = empty($data['background']) ? 'sogo-grey' : $data['background']; + + return $data; + break; } break; } diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index f62819a2..fe7a6701 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -40,6 +40,7 @@ $globalVariables = [ 'ui_texts' => $UI_TEXTS, 'css_path' => '/cache/'.basename($CSSPath), 'logo' => customize('get', 'main_logo'), + 'favicon' => customize('get', 'favicon'), '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 fde1507f..fad6e101 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -125,6 +125,14 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi if (isset($_POST["reset_main_logo"])) { customize('delete', 'main_logo'); } + if (isset($_POST["submit_favicon"])) { + if ($_FILES['favicon']['error'] == 0) { + customize('add', 'favicon', $_FILES); + } + } + if (isset($_POST["reset_favicon"])) { + customize('delete', 'favicon'); + } // Some actions will not be available via API if (isset($_POST["license_validate_now"])) { license('verify'); diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 5e6d72e7..2fcc1c9b 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -210,6 +210,30 @@ $FIDO2_USER_PRESENT_FLAG = true; $FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm'); +$SOGO_PALETTES = array( + 'sogo-green', + 'sogo-blue', + 'sogo-grey', + 'red', + 'pink', + 'purple', + 'deep-purple', + 'indigo', + 'blue', + 'light-blue', + 'cyan', + 'teal', + 'green', + 'light-green', + 'lime', + 'yellow', + 'amber', + 'orange', + 'deep-orange', + 'brown', + 'grey', + 'blue-grey' +); // Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing $RSPAMD_MAPS = array( diff --git a/data/web/json_api.php b/data/web/json_api.php index ec028fe4..1e258a10 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -288,18 +288,18 @@ if (isset($_GET['query'])) { case "domain-admin": process_add_return(domain_admin('add', $attr)); break; - case "sso": - switch ($object) { - case "domain-admin": - $data = domain_admin_sso('issue', $attr); - if($data) { - echo json_encode($data); - exit(0); - } - process_add_return($data); - break; - } - break; + case "sso": + switch ($object) { + case "domain-admin": + $data = domain_admin_sso('issue', $attr); + if($data) { + echo json_encode($data); + exit(0); + } + process_add_return($data); + break; + } + break; case "admin": process_add_return(admin('add', $attr)); break; @@ -1744,6 +1744,9 @@ if (isset($_GET['query'])) { case "rlhash": echo ratelimit('delete', null, implode($items)); break; + case "sogo_theme": + process_delete_return(customize('delete', 'sogo_theme', $items)); + break; // return no route found if no case is matched default: http_response_code(404); @@ -1938,6 +1941,9 @@ if (isset($_GET['query'])) { case "ip_check": process_edit_return(customize('edit', 'ip_check', $attr)); break; + case "sogo_theme": + process_edit_return(customize('edit', 'sogo_theme', $attr)); + break; case "self": if ($_SESSION['mailcow_cc_role'] == "domainadmin") { process_edit_return(domain_admin('edit', $attr)); diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 4bd4b3fa..2d936e22 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -145,6 +145,7 @@ "ays": "Soll der Vorgang wirklich ausgeführt werden?", "ban_list_info": "Übersicht ausgesperrter Netzwerke: Netzwerk (verbleibende Bannzeit) - [Aktionen].
IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.
Rote Labels sind Indikatoren für aktive Blacklist-Einträge.", "change_logo": "Logo ändern", + "change_favicon": "Favicon ändern", "configuration": "Konfiguration", "convert_html_to_text": "Konvertiere HTML zu reinem Text", "credentials_transport_warning": "Warnung: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.", @@ -216,6 +217,7 @@ "loading": "Bitte warten...", "login_time": "Zeit", "logo_info": "Die hochgeladene Grafik wird für die Navigationsleiste auf eine Höhe von 40px skaliert. Für die Darstellung auf der Login-Maske beträgt die skalierte Breite maximal 250px. Eine frei skalierbare Grafik (etwa SVG) wird empfohlen.", + "favicon_info": "Das Bild muss eine PNG- oder ICO-Datei mit den Abmessungen 32 x 32, 128 x 128, 180 x 180, 192 x 192 oder 256 x 256 sein. SOGo muss nachdem ändern neugestartet werden.", "lookup_mx": "Ziel mit MX vergleichen (Regex, etwa .*google\\.com, um alle Ziele mit MX *google.com zu routen)", "main_name": "\"mailcow UI\" Name", "merged_vars_hint": "Ausgegraute Reihen wurden aus der Datei vars.(local.)inc.php gelesen und können hier nicht verändert werden.", @@ -306,6 +308,8 @@ "sender": "Sender", "service": "Dienst", "service_id": "Service", + "sogo_theme": "SOGo Theme", + "sogo_theme_info": "SOGo muss nachdem ändern neugestartet werden.", "source": "Quelle", "spamfilter": "Spamfilter", "subject": "Betreff", @@ -1040,6 +1044,7 @@ "relayhost_added": "Map-Eintrag %s wurde hinzugefügt", "relayhost_removed": "Map-Eintrag %s wurde entfernt", "reset_main_logo": "Standardgrafik wurde wiederhergestellt", + "reset_favicon": "Standard favicon wurde wiederhergestellt", "resource_added": "Ressource %s wurde angelegt", "resource_modified": "Änderungen an Ressource %s wurden gespeichert", "resource_removed": "Ressource %s wurde entfernt", @@ -1052,6 +1057,8 @@ "template_modified": "Änderungen am Template %s wurden gespeichert", "template_removed": "Template ID %s wurde gelöscht", "sogo_profile_reset": "ActiveSync-Gerät des Benutzers %s wurde zurückgesetzt", + "sogo_theme_modified": "SOGo Theme wurde gespeichert", + "sogo_theme_removed": "SOGo Theme wurde entfernt", "tls_policy_map_entry_deleted": "TLS-Richtlinie mit der ID %s wurde gelöscht", "tls_policy_map_entry_saved": "TLS-Richtlinieneintrag \"%s\" wurde gespeichert", "ui_texts": "Änderungen an UI-Texten", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index df83987c..146ad1d7 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -147,6 +147,7 @@ "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", + "change_favicon": "Change favicon", "configuration": "Configuration", "convert_html_to_text": "Convert HTML to plain text", "credentials_transport_warning": "Warning: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.", @@ -218,6 +219,7 @@ "loading": "Please wait...", "login_time": "Login time", "logo_info": "Your image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. A scalable graphic is highly recommended.", + "favicon_info": "The image has to be a PNG or ICO file in the dimensions 32 x 32, 128 x 128, 180 x 180, 192 x 192, or 256 x 256. Restart SOGo after changing the favicon", "lookup_mx": "Destination is a regular expression to match against MX name (.*google\\.com to route all mail targeted to a MX ending in google.com over this hop)", "main_name": "\"mailcow UI\" name", "merged_vars_hint": "Greyed out rows were merged from vars.(local.)inc.php and cannot be modified.", @@ -311,6 +313,11 @@ "sender": "Sender", "service": "Service", "service_id": "Service ID", + "sogo_theme": "SOGo Theme", + "sogo_theme_info": "Restart SOGo after changing the theme.", + "sogo_theme_primary": "Primary Color:", + "sogo_theme_accent": "Accent Color:", + "sogo_theme_background": "Background Color:", "source": "Source", "spamfilter": "Spam filter", "subject": "Subject", @@ -1047,6 +1054,7 @@ "relayhost_added": "Map entry %s has been added", "relayhost_removed": "Map entry %s has been removed", "reset_main_logo": "Reset to default logo", + "reset_favicon": "Reset to default favicon", "resource_added": "Resource %s has been added", "resource_modified": "Changes to mailbox %s have been saved", "resource_removed": "Resource %s has been removed", @@ -1056,6 +1064,8 @@ "settings_map_added": "Added settings map entry", "settings_map_removed": "Removed settings map ID %s", "sogo_profile_reset": "SOGo profile for user %s was reset", + "sogo_theme_modified": "SOGo Theme has been modified", + "sogo_theme_removed": "SOGo Theme has been removed", "template_added": "Added template %s", "template_modified": "Changes to template %s have been saved", "template_removed": "Template ID %s has been deleted", diff --git a/data/web/templates/admin/tab-config-customize.twig b/data/web/templates/admin/tab-config-customize.twig index 766c0b6a..bd8e470a 100644 --- a/data/web/templates/admin/tab-config-customize.twig +++ b/data/web/templates/admin/tab-config-customize.twig @@ -7,31 +7,89 @@ {{ lang.admin.customize }}
- {{ lang.admin.change_logo }}
-

{{ lang.admin.logo_info }}

-
-

-
- -

-
- {% if logo %} -
-
-
+
+
+ {{ lang.admin.change_logo }}
+

{{ lang.admin.logo_info }}

+
+

+
+ +

+
+ {% if logo %} +
mailcow logo -
+
{{ logo_specs.geometry.width }}x{{ logo_specs.geometry.height }} px - {{ logo_specs.mimetype }} + {{ logo_specs.mimetype }} {{ logo_specs.fileSize }}
-

+ {% endif %} +
+
+ {{ lang.admin.change_favicon }}
+

{{ lang.admin.favicon_info|raw }}

+
+

+
+ +

+
+ {% if favicon %} +
+ mailcow favicon +
+ {{ favicon_specs.geometry.width }}x{{ favicon_specs.geometry.height }} px + {{ favicon_specs.mimetype }} + {{ favicon_specs.fileSize }} +
+
+
+

+
+ {% endif %} +
+
+ {% if not skip_sogo %} + {{ lang.admin.sogo_theme }}
+
+
+
+ + +
+
+ + +
+
+ +
+

{{ lang.admin.sogo_theme_info }}

+

+ + +

+
{% endif %} {{ lang.admin.ip_check }}
diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 06c47bd2..9c212954 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -23,8 +23,8 @@ } - - + +
diff --git a/docker-compose.yml b/docker-compose.yml index 23bd308f..e8b3dd95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -169,7 +169,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.117 + image: mailcow/sogo:1.118 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -510,7 +510,7 @@ services: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:2.03 + image: mailcow/dockerapi:2.04 security_opt: - label=disable restart: always