diff --git a/README.md b/README.md index 61ff4c5..3864470 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,6 @@ # ns8-sogo -This is a template module for [NethServer 8](https://github.com/NethServer/ns8-core). -To start a new module from it: - -1. Click on [Use this template](https://github.com/NethServer/ns8-sogo/generate). - Name your repo with `ns8-` prefix (e.g. `ns8-mymodule`). - Do not end your module name with a number, like ~~`ns8-baaad2`~~! - -1. Clone the repository, enter the cloned directory and - [configure your GIT identity](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_your_identity) - -1. Rename some references inside the repo: - ``` - modulename=$(basename $(pwd) | sed 's/^ns8-//') - git mv imageroot/systemd/user/sogo.service imageroot/systemd/user/${modulename}.service - git mv tests/sogo.robot tests/${modulename}.robot - sed -i "s/sogo/${modulename}/g" $(find .github/ * -type f) - git commit -a -m "Repository initialization" - ``` - -1. Edit this `README.md` file, by replacing this section with your module - description - -1. Adjust `.github/workflows` to your needs. `clean-registry.yml` might - need the proper list of image names to work correctly. Unused workflows - can be disabled from the GitHub Actions interface. - -1. Commit and push your local changes +This module intends to istall the SOGo groupware : https://www.sogo.nu/ ## Install @@ -39,46 +13,99 @@ Output example: {"module_id": "sogo1", "image_name": "sogo", "image_url": "ghcr.io/nethserver/sogo:latest"} +## Get the configuration +You can retrieve the configuration with + +api-cli run get-configuration --agent module/sogo1 + ## Configure Let's assume that the sogo instance is named `sogo1`. + Launch `configure-module`, by setting the following parameters: -- ``: -- ``: -- ... +- `host`: a fully qualified domain name for the application +- `lets_encrypt`: enable or disable Let's Encrypt certificate (true/false) +- `mail_server`: the module UUID of the the mail server (only on NS8), for example `24c52316-5af5-4b4d-8b0f-734f9ee9c1d9` +- `mail_domain`: the mail domain used for user IMAP login and SOGo user identifier. It must correspond to a valid mail domain handled by `mail_server` where user names are valid mail addresses too +- `ldap_domain`: a Ldap domain where to authenticate the users, it could be different than hostname. The ldap_domain is used to find an existing LDAP through LDAP proxy +- `admin_users`: the administrator of SOGo, a comma separated list (user1,user2) +- `workers_count`: The number of workers for SOGo; you need to adapt it to the numbers of users +- `auxiliary_account`: Allow users to set other email accounts inside the SOGo webmail (boolean) +- `activesync`: Enable the activesync protocom (boolean) +- `dav`: Enable the DAV protocol (caldav,cardav) (boolean) Example: - api-cli run module/sogo1/configure-module --data '{}' + api-cli run configure-module --agent module/sogo1 --data - <&2 # Redirect any output to the journal (stderr) + +# Prepare an initialization script that restores the dump file +mkdir -vp initdb.d +mv -v sogo.sql initdb.d +#do the bash file to restore and exit once done +cat - >initdb.d/zz_sogo_restore.sh <<'EOS' +# Print additional information: +mysql --version +# The script is sourced, override entrypoint args and exit: +set -- true +docker_temp_server_stop +exit 1 +EOS + +# once we exit we remove initdb.d +trap 'rm -rfv initdb.d/' EXIT + +# we start a container to initiate a database and load the dump +# at the end of sogo_restore.sh the dump is loaded and +# we exit the container +podman run \ + --rm \ + --interactive \ + --network=none \ + --volume=./initdb.d:/docker-entrypoint-initdb.d:z \ + --volume mysql-data:/var/lib/mysql/:Z \ + --env MARIADB_ROOT_PASSWORD=Nethesis,1234 \ + --env MARIADB_DATABASE=sogo \ + --env MARIADB_USER=sogo \ + --env MARIADB_PASSWORD=sogo \ + --replace --name=restore_db \ + ${MARIADB_IMAGE} diff --git a/imageroot/actions/restore-module/50call-configure-module b/imageroot/actions/restore-module/50call-configure-module new file mode 100755 index 0000000..33749fb --- /dev/null +++ b/imageroot/actions/restore-module/50call-configure-module @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import sys +import json +import agent +import os + +request = json.load(sys.stdin) +renv = request['environment'] + +configure_retval = agent.tasks.run(agent_id=os.environ['AGENT_ID'], action='configure-module', data={ + "ldap_domain": renv["LDAP_DOMAIN"], + "admin_users": renv["ADMIN_USERS"], + "mail_server": renv["MAIL_SERVER"], + "mail_domain": renv["MAIL_DOMAIN"], + "lets_encrypt": renv["TRAEFIK_LETS_ENCRYPT"], + "host": renv["TRAEFIK_HOST"], + "workers_count": renv["WOWORKERSCOUNT"], + "auxiliary_account": renv["AUXILIARYACCOUNT"], + "activesync": renv["ACTIVESYNC"], + "dav": renv["DAV"], + +}) +agent.assert_exp(configure_retval['exit_code'] == 0, "The configure-module subtask failed!") diff --git a/imageroot/bin/discover-ldap b/imageroot/bin/discover-ldap new file mode 100755 index 0000000..b86a0d1 --- /dev/null +++ b/imageroot/bin/discover-ldap @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2022 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +# +# Find settings for LDAP service +# + +import os +import sys +import json +import agent +from agent.ldapproxy import Ldapproxy + +udomname = os.environ.get('LDAP_DOMAIN','') + +try: + odom = Ldapproxy().get_domain(udomname) + 'host' in odom # Throw exception if odom is None +except: + # During restore the domain could be unavailable. Use a fallback + # configuration, pointing to nowhere, just to set the variables. + # Once the domain becomes available, the event will fix them. + odom = { + 'host': '127.0.0.1', + 'port': 20000, + 'schema': 'rfc2307', + 'location': 'internal', + 'base_dn': 'dc=sogo,dc=invalid', + 'bind_dn': 'cn=example,dc=sogo,dc=invalid', + 'bind_password': 'invalid', + } + +tmpfile = "discover.env." + str(os.getpid()) + +with open(tmpfile, "w") as denv: + print('SOGO_LDAP_PORT=' + str(odom['port']), file=denv) + print('SOGO_LDAP_USER=' + odom['bind_dn'], file=denv) + print('SOGO_LDAP_HOST=' + odom['host'], file=denv) + print('SOGO_LDAP_PASS=' + odom['bind_password'], file=denv) + print('SOGO_LDAP_SCHEMA=' + odom['schema'], file=denv) + print('SOGO_LDAP_BASE=' + odom['base_dn'], file=denv) + +os.replace(tmpfile, "discovery_ldap.env") diff --git a/imageroot/bin/discover-service b/imageroot/bin/discover-service new file mode 100755 index 0000000..e8d9a8e --- /dev/null +++ b/imageroot/bin/discover-service @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +# +# Find settings (port, ip, ..) of mail service among the cluster +# + +import os +import sys +import json +import agent + +# This script must rely on local node resources to ensure service startup +# even if the leader node is not reachable: connect to local Redis +# replica. +rdb = agent.redis_connect(use_replica=True) + +imap = agent.list_service_providers(rdb, 'imap', 'tcp', { + 'module_uuid': os.environ['MAIL_SERVER'] +}) + +if len(imap) != 1: + # Only ONE provider for the same UUID is allowed. Zero means it is + # missing. More than one means the DB is inconsistent. + print(agent.SD_ERR + "Cannot find the imap service of my Mail module instance", os.environ['MAIL_SERVER'], file=sys.stderr) + sys.exit(4) + +smtp = agent.list_service_providers(rdb, 'submission', 'tcp', { + 'module_uuid': os.getenv('MAIL_SERVER', '') +}) + +if len(smtp) != 1: + # Only ONE provider for the same UUID is allowed. Zero means it is + # missing. More than one means the DB is inconsistent. + print(agent.SD_ERR + "Cannot find the submission service of my Mail module instance", os.environ['MAIL_SERVER'], file=sys.stderr) + sys.exit(5) + +imap_port = imap[0]['port'] +imap_server = imap[0]['host'] +user_domain = os.getenv('MAIL_DOMAIN', imap[0]['user_domain']) + +smtp_port = smtp[0]['port'] +smtp_server = smtp[0]['host'] + +envfile = "discovery_mail.env" + +# Using .tmp suffix: do not overwrite the target file until the new one is +# saved to disk: +with open(envfile + ".tmp", "w") as efp: + print(f"SOGO_IMAP_PORT={imap_port}", file=efp) + print(f"SOGO_SMTP_PORT={smtp_port}", file=efp) + print(f"SOGO_DEFAULT_HOST={imap_server}", file=efp) + print(f"SOGO_SMTP_SERVER={smtp_server}", file=efp) + +# Commit changes by replacing the existing envfile: +os.replace(envfile + ".tmp", envfile) + diff --git a/imageroot/bin/discover-smarthost b/imageroot/bin/discover-smarthost deleted file mode 100755 index da3a22b..0000000 --- a/imageroot/bin/discover-smarthost +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 - -# -# Copyright (C) 2023 Nethesis S.r.l. -# SPDX-License-Identifier: GPL-3.0-or-later -# - -import sys -import agent -import os - -# Connect the local Redis replica. This is necessary to consistently start -# the service if the leader node is not reachable: -rdb = agent.redis_connect(use_replica=True) -smtp_settings = agent.get_smarthost_settings(rdb) - -envfile = "smarthost.env" - -# Using .tmp suffix: do not overwrite the target file until the new one is -# saved to disk: -with open(envfile + ".tmp", "w") as efp: - # HINT for sogo: adjust variable names as needed - print(f"SMTP_ENABLED={'1' if smtp_settings['enabled'] else ''}", file=efp) - print(f"SMTP_HOST={smtp_settings['host']}", file=efp) - print(f"SMTP_PORT={smtp_settings['port']}", file=efp) - print(f"SMTP_USERNAME={smtp_settings['username']}", file=efp) - print(f"SMTP_PASSWORD={smtp_settings['password']}", file=efp) - print(f"SMTP_ENCRYPTION={smtp_settings['encrypt_smtp']}", file=efp) - print(f"SMTP_TLSVERIFY={'1' if smtp_settings['tls_verify'] else ''}", file=efp) - -# Commit changes by replacing the existing envfile: -os.replace(envfile + ".tmp", envfile) - -# NOTE: The generated envfile can be included in the service container -# with `podman run --env-file` or in Systemd unit with -# `Environment=-%S/state/smarthost.env` diff --git a/imageroot/bin/expand-configuration b/imageroot/bin/expand-configuration new file mode 100755 index 0000000..6894aab --- /dev/null +++ b/imageroot/bin/expand-configuration @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2022 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import os +import json +import agent +import agent.tasks +import socket + +from jinja2 import Environment, FileSystemLoader, select_autoescape + +files =["config/sogo.conf","config/SOGo.conf"] +for f in files: + try: + os.remove(f) + except FileNotFoundError: + pass + +jenv = Environment( + loader=FileSystemLoader(os.getenv("AGENT_INSTALL_DIR")+"/templates"), + autoescape=select_autoescape(), + keep_trailing_newline=True, +) + +properties = { + "admin_users" : os.environ['ADMIN_USERS'], + "ldap_schema" : os.environ['SOGO_LDAP_SCHEMA'], + "ldap_port" : int(os.environ['SOGO_LDAP_PORT']), + "ldap_user" : os.environ['SOGO_LDAP_USER'], + "ldap_password" : os.environ['SOGO_LDAP_PASS'], + "ldap_base" : os.environ['SOGO_LDAP_BASE'], + "imap_port":os.environ['SOGO_IMAP_PORT'], + "smtp_port":os.environ['SOGO_SMTP_PORT'], + "mail_server":os.environ['SOGO_SMTP_SERVER'], + "mail_domain":os.environ['MAIL_DOMAIN'], + "workers_count":os.environ['WOWORKERSCOUNT'], + "auxiliary_account": 'YES' if os.environ['AUXILIARYACCOUNT'] == 'True' else 'NO', + "active_sync": True if os.environ['ACTIVESYNC'] == 'True' else False, + "dav": True if os.environ['DAV'] == 'True' else False, + "draftsfolder": os.environ['DRAFTSFOLDER'], + "sogofolderssendemailnotifications": os.environ['SOGOFOLDERSSENDEMAILNOTIFICATIONS'], + "sogoaclssendemailnotifications": os.environ['SOGOACLSSENDEMAILNOTIFICATIONS'], + "sogoappointmentsendemailnotifications": os.environ['SOGOAPPOINTMENTSENDEMAILNOTIFICATIONS'], + "sogoenableemailalarms": os.environ['SOGOENABLEEMAILALARMS'], + "sogointernalsyncinterval": os.environ['SOGOINTERNALSYNCINTERVAL'], + "sogomaximumpinginterval": os.environ['SOGOMAXIMUMPINGINTERVAL'], + "sogomaximumsyncinterval": os.environ['SOGOMAXIMUMSYNCINTERVAL'], + "sogomaximumsyncresponsesize": os.environ['SOGOMAXIMUMSYNCRESPONSESIZE'], + "sogomaximumsyncwindowsize": os.environ['SOGOMAXIMUMSYNCWINDOWSIZE'], + "sentfolder": os.environ['SENTFOLDER'], + "sessionduration": os.environ['SESSIONDURATION'], + "sxvmemlimit": os.environ['SXVMEMLIMIT'], + "timezone": os.environ['TIMEZONE'], + "trashfolder": os.environ['TRASHFOLDER'], + "wowatchdogrequesttimeout": os.environ['WOWATCHDOGREQUESTTIMEOUT'] +} + +template = jenv.get_template('sogo.conf') +output = template.render(properties) +with open("config/sogo.conf","w") as f: + f.write(output) + +# build httpd config +properties = { + "domain": os.environ['TRAEFIK_HOST'], + "active_sync": True if os.environ['ACTIVESYNC'] == 'True' else False, + "dav": True if os.environ['DAV'] == 'True' else False +} + +template = jenv.get_template('SOGo.conf') +output = template.render(properties) +with open("config/SOGo.conf","w") as f: + f.write(output) + +# build cron config +properties = { + "backuptime": os.environ['BACKUPTIME'], + "sessionduration": os.environ['SESSIONDURATION'] +} + +template = jenv.get_template('cron.conf') +output = template.render(properties) +with open("config/cron-sogo","w") as f: + f.write(output) diff --git a/imageroot/bin/module-cleanup-state b/imageroot/bin/module-cleanup-state new file mode 100755 index 0000000..d713a14 --- /dev/null +++ b/imageroot/bin/module-cleanup-state @@ -0,0 +1,8 @@ +#!/bin/bash + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +rm -vf sogo.sql diff --git a/imageroot/bin/module-dump-state b/imageroot/bin/module-dump-state new file mode 100755 index 0000000..b4e4616 --- /dev/null +++ b/imageroot/bin/module-dump-state @@ -0,0 +1,21 @@ +#!/bin/bash + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +set -e + +if ! systemctl --user -q is-active sogo.service; then + exit 0 +fi + +podman exec mariadb-app mysqldump \ + --databases sogo \ + --default-character-set=utf8mb4 \ + --skip-dump-date \ + --ignore-table=mysql.event \ + --single-transaction \ + --quick \ + --add-drop-table > sogo.sql diff --git a/imageroot/bin/reveal-master-secret b/imageroot/bin/reveal-master-secret new file mode 100755 index 0000000..3e75dd3 --- /dev/null +++ b/imageroot/bin/reveal-master-secret @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import os +import agent + +rdb = agent.redis_connect() + +# retrieve mail module_id from module_uuid +mail_server = os.environ["MAIL_SERVER"] +providers = agent.list_service_providers(rdb, 'imap', 'tcp', { + 'module_uuid': mail_server, +}) +mail_id = providers[0]["module_id"] + +response = agent.tasks.run(f"module/{mail_id}", action='reveal-master-credentials') +agent.assert_exp(response['exit_code'] == 0) +vmail_password = response['output']['password'] + +os.umask(0o77) +f = open("./config/sieve.creds", "w", encoding="utf-8") +f.write("vmail:"+vmail_password) +f.close() diff --git a/imageroot/etc/state-include.conf b/imageroot/etc/state-include.conf new file mode 100644 index 0000000..8a491af --- /dev/null +++ b/imageroot/etc/state-include.conf @@ -0,0 +1,7 @@ +# +# SOGo state/backup include patterns for Restic +# Syntax reference: https://pkg.go.dev/path/filepath#Glob +# Restic --files-from: https://restic.readthedocs.io/en/stable/040_backup.html#including-files +# +state/sogo.sql +state/backups diff --git a/imageroot/systemd/user/mariadb-app.service b/imageroot/systemd/user/mariadb-app.service new file mode 100644 index 0000000..2c7b3d3 --- /dev/null +++ b/imageroot/systemd/user/mariadb-app.service @@ -0,0 +1,39 @@ +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +[Unit] +Description=Podman mariadb-app.service +BindsTo=sogo.service +After=sogo.service + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +EnvironmentFile=%S/state/environment +Restart=always +TimeoutStopSec=70 +ExecStartPre=/bin/rm -f %t/mariadb-app.pid %t/mariadb-app.ctr-id +ExecStart=/usr/bin/podman run --conmon-pidfile %t/mariadb-app.pid \ + --cidfile %t/mariadb-app.ctr-id --cgroups=no-conmon \ + --pod-id-file %t/sogo.pod-id --replace -d --name mariadb-app \ + --env-file=%S/state/environment \ + --volume mysql-data:/var/lib/mysql/:Z \ + --env MARIADB_ROOT_PASSWORD=Nethesis,1234 \ + --env MARIADB_DATABASE=sogo \ + --env MARIADB_USER=sogo \ + --env MARIADB_PASSWORD=Nethesis,1234 \ + --env MARIADB_AUTO_UPGRADE=1 \ + ${MARIADB_IMAGE} \ + --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci +ExecStartPost=/usr/bin/podman exec mariadb-app /bin/bash -c 'printf "[client] \npassword=Nethesis,1234" > /root/.my.cnf' +ExecStartPost=/usr/bin/podman exec mariadb-app /bin/bash -c "while ! mysqladmin ping -h localhost -P 3306 -u root; do sleep 1; done" +ExecStop=/usr/bin/podman stop --ignore --cidfile %t/mariadb-app.ctr-id -t 10 +ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/mariadb-app.ctr-id +ExecReload=/usr/bin/podman kill -s HUP mariadb-app +SyslogIdentifier=%u +PIDFile=%t/mariadb-app.pid +Type=forking + +[Install] +WantedBy=default.target diff --git a/imageroot/systemd/user/sogo-app.service b/imageroot/systemd/user/sogo-app.service new file mode 100644 index 0000000..6f20443 --- /dev/null +++ b/imageroot/systemd/user/sogo-app.service @@ -0,0 +1,43 @@ +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +[Unit] +Description=Podman sogo-app.service +BindsTo=sogo.service +After=sogo.service mariadb-app.service + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +EnvironmentFile=%S/state/environment +EnvironmentFile=-%S/state/discovery_mail.env +EnvironmentFile=-%S/state/discovery_ldap.env +WorkingDirectory=%S/state +Restart=always +TimeoutStopSec=70 +ExecStartPre=/usr/bin/bash -c "/bin/mkdir -p {config,backups}" +ExecStartPre=/bin/rm -f %t/sogo-app.pid %t/sogo-app.ctr-id +ExecStartPre=/usr/local/bin/runagent discover-ldap +ExecStartPre=/usr/local/bin/runagent discover-service +ExecStartPre=/usr/local/bin/runagent expand-configuration +ExecStartPre=/usr/local/bin/runagent reveal-master-secret +ExecStartPost=/usr/bin/bash -c "while ! /usr/bin/podman exec sogo-app /usr/bin/curl http://127.0.0.1:20001/SOGo ; do sleep 3 ; done" +ExecStart=/usr/bin/podman run --conmon-pidfile %t/sogo-app.pid \ + --cidfile %t/sogo-app.ctr-id --cgroups=no-conmon \ + --pod-id-file %t/sogo.pod-id --replace -d --name sogo-app \ + --volume ./config/sogo.conf:/etc/sogo/sogo.conf:Z \ + --volume ./config/cron-sogo:/etc/cron.d/cron-sogo:Z \ + --volume ./config/sieve.creds:/etc/sogo/sieve.creds:Z \ + --volume ./config/SOGo.conf:/etc/httpd/conf/extra/SOGo.conf:Z \ + --volume ./backups:/etc/sogo/backups:Z \ + ${SOGO_SERVER_IMAGE} +ExecStop=/usr/bin/podman stop --ignore --cidfile %t/sogo-app.ctr-id -t 10 +ExecReload=/usr/bin/podman kill -s HUP sogo-app +SyslogIdentifier=%u +ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/sogo-app.ctr-id +PIDFile=%t/sogo-app.pid +Type=forking + +[Install] +WantedBy=default.target diff --git a/imageroot/systemd/user/sogo.service b/imageroot/systemd/user/sogo.service index ac13329..2853e82 100644 --- a/imageroot/systemd/user/sogo.service +++ b/imageroot/systemd/user/sogo.service @@ -7,27 +7,28 @@ # This systemd unit starts a sogo instance using Podman. # Most parts of this file come from podman-generate-systemd. # + [Unit] -Description=sogo server +Description=Podman sogo.service +Requires=mariadb-app.service sogo-app.service +Before=mariadb-app.service sogo-app.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n -EnvironmentFile=%S/state/environment -WorkingDirectory=%S/state +EnvironmentFile=-%S/state/environment Restart=always -ExecStartPre=/bin/rm -f %t/sogo.pid %t/sogo.ctr-id -ExecStartPre=-runagent discover-smarthost -ExecStart=/usr/bin/podman run \ - --detach \ - --conmon-pidfile=%t/sogo.pid \ - --cidfile=%t/sogo.ctr-id \ - --cgroups=no-conmon \ - --replace --name=%N \ - --publish=127.0.0.1:${TCP_PORT}:8080 \ - --env-file=smarthost.env \ - ${ECHO_SERVER_IMAGE} -ExecStop=/usr/bin/podman stop --ignore --cidfile %t/sogo.ctr-id -t 10 -ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/sogo.ctr-id +TimeoutStopSec=70 +ExecStartPre=/bin/rm -f %t/sogo.pid %t/sogo.pod-id +ExecStartPre=/usr/bin/podman pod create --infra-conmon-pidfile %t/sogo.pid \ + --pod-id-file %t/sogo.pod-id \ + --name sogo \ + --publish 127.0.0.1:${TCP_PORT}:20001 \ + --replace \ + --network=slirp4netns:allow_host_loopback=true \ + --add-host=accountprovider:10.0.2.2 +ExecStart=/usr/bin/podman pod start --pod-id-file %t/sogo.pod-id +ExecStop=/usr/bin/podman pod stop --ignore --pod-id-file %t/sogo.pod-id -t 10 +ExecStopPost=/usr/bin/podman pod rm --ignore -f --pod-id-file %t/sogo.pod-id PIDFile=%t/sogo.pid Type=forking diff --git a/imageroot/templates/SOGo.conf b/imageroot/templates/SOGo.conf new file mode 100644 index 0000000..792aea7 --- /dev/null +++ b/imageroot/templates/SOGo.conf @@ -0,0 +1,118 @@ +Alias /SOGo.woa/WebServerResources/ \ + /usr/lib/GNUstep/SOGo/WebServerResources/ +Alias /SOGo/WebServerResources/ \ + /usr/lib/GNUstep/SOGo/WebServerResources/ + + + AllowOverride None + + + Order deny,allow + Allow from all + + = 2.4> + Require all granted + + + # Explicitly allow caching of static content to avoid browser specific behavior. + # A resource's URL MUST change in order to have the client load the new version. + + ExpiresActive On + ExpiresDefault "access plus 1 year" + + + +# Don't send the Referer header for cross-origin requests +Header always set Referrer-Policy "same-origin" + + + # Don't cache dynamic content + Header set Cache-Control "max-age=0, no-cache, no-store" + + +## Uncomment the following to enable proxy-side authentication, you will then +## need to set the "SOGoTrustProxyAuthentication" SOGo user default to YES and +## adjust the "x-webobjects-remote-user" proxy header in the "Proxy" section +## below. +# +## For full proxy-side authentication: +# +# AuthType XXX +# Require valid-user +# SetEnv proxy-nokeepalive 1 +# Allow from all +# +# +## For proxy-side authentication only for CardDAV and GroupDAV from external +## clients: +# +# AuthType XXX +# Require valid-user +# SetEnv proxy-nokeepalive 1 +# Allow from all +# + +ProxyRequests Off +ProxyPreserveHost On +SetEnv proxy-nokeepalive 1 + +# Uncomment the following lines if you experience "Bad gateway" errors with mod_proxy +#SetEnv proxy-initial-not-pooled 1 +#SetEnv force-proxy-request-1.0 1 + +# When using CAS, you should uncomment this and install cas-proxy-validate.py +# in /usr/lib/cgi-bin to reduce server overloading +# +# ProxyPass /SOGo/casProxy http://localhost/cgi-bin/cas-proxy-validate.py +# +# Order deny,allow +# Allow from your-cas-host-addr +# + +# Redirect / to /SOGo +RedirectMatch ^/$ https://{{domain}}/SOGo + +{% if active_sync %} +# Enable to use Microsoft ActiveSync support +# Note that you MUST have many sogod workers to use ActiveSync. +# See the SOGo Installation and Configuration guide for more details. +# +ProxyPass /Microsoft-Server-ActiveSync \ + http://127.0.0.1:20000/SOGo/Microsoft-Server-ActiveSync \ + retry=60 connectiontimeout=5 timeout=360 +{% endif %} + +ProxyPass /SOGo http://127.0.0.1:20000/SOGo retry=0 nocanon + + +## Adjust the following to your configuration +## and make sure to enable the headers module + + RequestHeader set "x-webobjects-server-port" "443" + SetEnvIf Host (.*) HTTP_HOST=$1 + RequestHeader set "x-webobjects-server-name" "%{HTTP_HOST}e" env=HTTP_HOST + RequestHeader set "x-webobjects-server-url" "https://%{HTTP_HOST}e" env=HTTP_HOST + +## When using proxy-side autentication, you need to uncomment and +## adjust the following line: + RequestHeader unset "x-webobjects-remote-user" +# RequestHeader set "x-webobjects-remote-user" "%{REMOTE_USER}e" env=REMOTE_USER + + RequestHeader set "x-webobjects-server-protocol" "HTTP/1.0" + + + AddDefaultCharset UTF-8 + + Order allow,deny + Allow from all + + +{% if dav %} +# For Apple autoconfiguration + + RewriteEngine On + RewriteRule ^/.well-known/caldav/?$ /SOGo/dav [R=301] + RewriteRule ^/.well-known/carddav/?$ /SOGo/dav [R=301] + RedirectMatch ^/(dav|cal|card)\$ /SOGo/dav/ + +{% endif %} diff --git a/imageroot/templates/cron.conf b/imageroot/templates/cron.conf new file mode 100644 index 0000000..563ec1d --- /dev/null +++ b/imageroot/templates/cron.conf @@ -0,0 +1,22 @@ +# Sogod cronjobs + +# Vacation messages expiration +# The credentials file should contain the sieve admin credentials (username:passwd) +0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds + +# Session cleanup - runs every minute +# - Ajust the nbMinutes parameter to suit your needs +# Example: Sessions without activity since 60 minutes will be dropped: +1 * * * * sogo /usr/sbin/sogo-tool expire-sessions {{sessionduration}} + +# Email alarms - runs every minutes +# If you need to use SMTP AUTH for outgoing mails, specify credentials to use +# with '-p /path/to/credentialsFile' (same format as the sieve credentials) +* * * * * sogo /usr/sbin/sogo-ealarms-notify > /dev/null 2>&1 + +# Daily backups +# - writes to /etc/sogo/backups/ by default +# - will keep 31 days worth of backups by default +# - runs once a day by default, but can run more frequently +# - make sure to set the path to sogo-backup.sh correctly +{{backuptime}} * * * sogo /usr/lib/sogo/scripts/sogo-backup.sh diff --git a/imageroot/templates/sogo.conf b/imageroot/templates/sogo.conf new file mode 100644 index 0000000..2a7f04b --- /dev/null +++ b/imageroot/templates/sogo.conf @@ -0,0 +1,164 @@ +{ + + /* 10 Database configuration (mysql) */ + SOGoProfileURL = "mysql://sogo:Nethesis,1234@127.0.0.1:3306/sogo/sogo_user_profile"; + OCSFolderInfoURL = "mysql://sogo:Nethesis,1234@127.0.0.1:3306/sogo/sogo_folder_info"; + OCSSessionsFolderURL = "mysql://sogo:Nethesis,1234@127.0.0.1:3306/sogo/sogo_sessions_folder"; + OCSEMailAlarmsFolderURL = "mysql://sogo:Nethesis,1234@127.0.0.1:3306/sogo/sogo_alarms_folder"; + + + /* 20 Mail */ + SOGoDraftsFolderName = "{{draftsfolder}}"; + SOGoSentFolderName = "{{sentfolder}}"; + SOGoTrashFolderName = "{{trashfolder}}"; + SOGoJunkFolderName = "Junk"; + SOGoIMAPServer = "{{mail_server}}:{{imap_port}}"; + SOGoSieveServer = "sieve://{{mail_server}}:4190"; + SOGoSMTPServer = "{{mail_server}}:{{smtp_port}}"; + SOGoMailDomain = "{{mail_domain}}"; + SOGoSMTPAuthenticationType = "PLAIN"; + SOGoMailingMechanism = "smtp"; + NGImap4ConnectionStringSeparator = "/"; + + /* 30 Notifications */ + SOGoFoldersSendEMailNotifications = {{sogofolderssendemailnotifications}}; + SOGoACLsSendEMailNotifications = {{sogoaclssendemailnotifications}}; + SOGoAppointmentSendEMailNotifications = {{sogoenableemailalarms}}; + SOGoEnableEMailAlarms = {{sogoenableemailalarms}}; + + + /* 40 Authentication */ + //SOGoPasswordChangeEnabled = YES; + + {% if ldap_schema == 'ad' %} + /* 45 AD authentication */ + SOGoUserSources =( + { + id = AD_Users; + type = ldap; + CNFieldName = displayName; + IDFieldName = sAMAccountName ; + UIDFieldName = sAMAccountName ; + IMAPLoginFieldName = sAMAccountName; + canAuthenticate = YES; + bindDN = "{{ldap_user}}"; + bindPassword = "{{ldap_password}}"; + baseDN = "{{ldap_base}}"; + bindFields = ( + sAMAccountName, + ); + hostname = ldap://accountprovider:{{ldap_port}}; + filter = "(objectClass='user') AND (sAMAccountType=805306368)"; + //MailFieldNames = ("userPrincipalName"); + scope = SUB; + displayName = "{{mail_domain}} users"; + isAddressBook = NO; + }, + { + id = AD_Groups; + type = ldap; + CNFieldName = name; + IDFieldName = sAMAccountName; + UIDFieldName = sAMAccountName; + canAuthenticate = YES; + bindDN = "{{ldap_user}}"; + bindPassword = "{{ldap_password}}"; + baseDN = "{{ldap_base}}"; + hostname = ldap://accountprovider:{{ldap_port}}; + filter = "(objectClass='group') AND (sAMAccountType=268435456)"; + //MailFieldNames = ("mail"); + scope = SUB; + displayName = "{{mail_domain}} groups"; + isAddressBook = NO; + } + ); + {% elif ldap_schema == 'rfc2307' %} + SOGoUserSources =( + { + id = groups; + type = ldap; + CNFieldName = cn; + UIDFieldName = cn; + IDFieldName = cn; + baseDN = "{{ldap_base}}"; + bindDN = "{{ldap_user}}"; + bindPassword = "{{ldap_password}}"; + scope = SUB; + canAuthenticate = YES; + MailFieldNames = ("mail"); + displayName = "{{mail_domain}} groups"; + hostname = ldap://accountprovider:{{ldap_port}}; + isAddressBook = NO; + }, + { + id = users; + type = ldap; + CNFieldName = displayName; + UIDFieldName = uid; + IDFieldName = uid; + bindFields = ( + uid + ); + IMAPLoginFieldName = uid; + baseDN = "{{ldap_base}}"; + bindDN = "{{ldap_user}}"; + bindPassword = "{{ldap_password}}"; + scope = SUB; + MailFieldNames = ("mail"); + canAuthenticate = YES; + displayName = "{{mail_domain}} users"; + hostname = ldap://accountprovider:{{ldap_port}}; + isAddressBook = NO; + } + ); + {% endif %} + + + + /* 50 Web Interface */ + SOGoVacationEnabled = YES; + SOGoForwardEnabled = YES; + SOGoSieveScriptsEnabled = YES; + SOGoMailAuxiliaryUserAccountsEnabled = {{auxiliary_account}}; + SOGoMailCustomFromEnabled = YES; + //SOGoFirstDayOfWeek = 1; + //SOGoMailReplyPlacement = "above"; + //SOGoMailSignaturePlacement = "above"; + + /* 60 General */ + SOGoTimeZone = {{timezone}}; + SOGoSuperUsernames = ({{admin_users}}); // This is an array - keep the parens! + SOGoMemcachedHost = "127.0.0.1"; + SxVMemLimit = {{sxvmemlimit}}; + SOGoEnablePublicAccess = YES; + + /* From Nethesis GNUStep configuration + Undocumented in sogo instalation manual */ + SOGoAppointmentSendEMailReceipts = YES; + + /* 70 Active Sync options and tuning */ + SOGoMaximumPingInterval = {{sogomaximumpinginterval}}; + SOGoMaximumSyncInterval = {{sogomaximumsyncinterval}}; + SOGoInternalSyncInterval = {{sogointernalsyncinterval}}; + SOGoMaximumSyncResponseSize = {{sogomaximumsyncresponsesize}}; + SOGoMaximumSyncWindowSize = {{sogomaximumsyncwindowsize}}; + + WOWatchDogRequestTimeout = {{wowatchdogrequesttimeout}}; + WOWorkersCount = {{workers_count}}; + + /* 75 enable cal- and carddav */ + SOGoAddressBookDAVAccessEnabled = {{dav}}; + SOGoCalendarDAVAccessEnabled = {{dav}}; + + /* 80 Debug */ + //SOGoDebugRequests = YES; + //SoDebugBaseURL = YES; + //ImapDebugEnabled = YES; + //LDAPDebugEnabled = YES; + //PGDebugEnabled = YES; + //MySQL4DebugEnabled = YES; + //SOGoUIxDebugEnabled = YES; + //WODontZipResponse = YES; + //SOGoEASDebugEnabled = YES; + //WOLogFile = "/var/log/sogo/sogo.log"; +} diff --git a/imageroot/update-module.d /20restart b/imageroot/update-module.d /20restart new file mode 100755 index 0000000..8f3d4b4 --- /dev/null +++ b/imageroot/update-module.d /20restart @@ -0,0 +1,14 @@ +#!/usr/bin/bash + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +set -e + +# Redirect any output to the journal (stderr) +exec 1>&2 + +# Restart if running, ignore if stopped +systemctl --user try-restart sogo.service diff --git a/ui/package.json b/ui/package.json index ea4cf27..14f118c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,7 +11,7 @@ "dependencies": { "@carbon/icons-vue": "^10.37.0", "@carbon/vue": "^2.40.0", - "@nethserver/ns8-ui-lib": "^0.1.22", + "@nethserver/ns8-ui-lib": "^0.1.27", "await-to-js": "^3.0.0", "axios": "^0.21.2", "carbon-components": "^10.41.0", diff --git a/ui/public/i18n/en/translation.json b/ui/public/i18n/en/translation.json index 0a04330..32ab911 100644 --- a/ui/public/i18n/en/translation.json +++ b/ui/public/i18n/en/translation.json @@ -24,7 +24,31 @@ "title": "Settings", "configure_instance": "Configure {instance}", "save": "Save", - "test_field": "Test field" + "SOGo_fqdn": "SOGo FQDN", + "lets_encrypt": "Let's Encrypt", + "disabled": "Disabled", + "enabled": "Enabled", + "mail_server_fqdn": "Mail server ", + "advanced": "Advanced", + "configuring": "Configuring", + "instance_configuration": "Configure Roundcube", + "choose_mail_server": "Select a domain", + "choose_the_mail_server_to_use": "Choose the domain suffix used for both identifying and initializing the user account and their mail address preferences", + "mail_server_is_not_valid": "This mail server cannot be used by Roundcube webmail", + "ldap_domain": "LDAP domain", + "choose_ldap_domain": "Choose the LDAP domain used for user authentication", + "choose_the_ldap_domain_to_authenticate_users": "Choose the LDAP user domain to authenticate users from an internal/external samba or openldap directory", + "adminList": "Administrator list", + "Write_administrator_list": "Write one administrator per line", + "dav_tips":"Dav allows to synchronize calendars and adressbooks", + "dav": "DAV", + "activesync": "ActiveSync", + "activesync_tips": "ActiveSync allows to syncronize mobile devices", + "workers_count": "Workers count", + "workers_count_tips": "Number of workers to use for SOGo", + "workers_count_placeholder": "Default value is 3", + "auxiliary_account": "Auxiliary email account", + "auxiliary_account_tips": "Users can add auxiliary email accounts to use with SOGo" }, "about": { "title": "About" diff --git a/ui/public/metadata.json b/ui/public/metadata.json index 7c15b22..43f6024 100644 --- a/ui/public/metadata.json +++ b/ui/public/metadata.json @@ -1,19 +1,19 @@ { "name": "sogo", "description": { - "en": "My sogo module" + "en": "SOGo is a free and modern scalable groupware server." }, - "categories": [], + "categories": ["collaboration"], "authors": [ { - "name": "Name Surname", - "email": "author@yourmail.org" + "name": "stephane de Labrusse", + "email": "stephdl@de-labrusse.fr" } ], "docs": { - "documentation_url": "https://docs.sogo.com/", + "documentation_url": "https://docs.nethserver.org/projects/ns8/en/latest/sogo.html", "bug_url": "https://github.com/NethServer/dev", - "code_url": "https://github.com/author/ns8-sogo" + "code_url": "https://github.com/NethServer/ns8-sogo" }, "source": "ghcr.io/nethserver/sogo" } diff --git a/ui/src/views/Settings.vue b/ui/src/views/Settings.vue index 10dd8c2..931c62b 100644 --- a/ui/src/views/Settings.vue +++ b/ui/src/views/Settings.vue @@ -22,16 +22,188 @@ - - + + + ref="host" + > + + + + + + + + + + + + + + + + + + { vm.watchQueryData(vm); @@ -110,9 +302,6 @@ export default { clearInterval(this.urlCheckInterval); next(); }, - created() { - this.getConfiguration(); - }, methods: { async getConfiguration() { this.loading.getConfiguration = true; @@ -157,45 +346,80 @@ export default { this.loading.getConfiguration = false; }, getConfigurationCompleted(taskContext, taskResult) { - this.loading.getConfiguration = false; const config = taskResult.output; + this.host = config.host; + this.isLetsEncryptEnabled = config.lets_encrypt; + this.isActivesyncEnabled = config.activesync; + this.isDavEnabled = config.dav; + this.admin_users = config.admin_users.split(",").join("\n"); + this.workers_count = config.workers_count.toString(); + this.auxiliary_account = config.auxiliary_account; - // TODO set configuration fields - // ... - - // TODO remove - console.log("config", config); + // force to reload mail_server value after dom update + this.$nextTick(() => { + const mail_server_tmp = config.mail_server; + const mail_domain_tmp = config.mail_domain; + if (mail_server_tmp && mail_domain_tmp) { + this.mail_server = mail_server_tmp + "," + mail_domain_tmp; + } else { + this.mail_server = ""; + } + this.ldap_domain = config.ldap_domain; + }); - // TODO focus first configuration field - this.focusElement("testField"); + this.mail_server_URL = config.mail_server_URL; + this.user_domains_list = config.user_domains_list; + this.loading.getConfiguration = false; + this.focusElement("host"); }, validateConfigureModule() { this.clearErrors(this); + let isValidationOk = true; + if (!this.host) { + this.error.host = "common.required"; - // TODO remove testField and validate configuration fields - if (!this.testField) { - // test field cannot be empty - this.error.testField = this.$t("common.required"); + if (isValidationOk) { + this.focusElement("host"); + } + isValidationOk = false; + } + if (!this.mail_server) { + this.error.mail_server = "common.required"; if (isValidationOk) { - this.focusElement("testField"); - isValidationOk = false; + this.focusElement("mail_server"); } + isValidationOk = false; + } + if (!this.ldap_domain) { + this.error.ldap_domain = "common.required"; + + if (isValidationOk) { + this.focusElement("ldap_domain"); + } + isValidationOk = false; } return isValidationOk; }, configureModuleValidationFailed(validationErrors) { this.loading.configureModule = false; + let focusAlreadySet = false; for (const validationError of validationErrors) { const param = validationError.parameter; - // set i18n error message this.error[param] = this.$t("settings." + validationError.error); + + if (!focusAlreadySet) { + this.focusElement(param); + focusAlreadySet = true; + } } }, async configureModule() { + this.error.test_imap = false; + this.error.test_smtp = false; const isValidationOk = this.validateConfigureModule(); if (!isValidationOk) { return; @@ -222,18 +446,29 @@ export default { `${taskAction}-completed-${eventId}`, this.configureModuleCompleted ); - + const tmparray = this.mail_server.split(","); + const mail_server_tmp = tmparray[0]; + const mail_domain_tmp = tmparray[1]; const res = await to( this.createModuleTaskForApp(this.instanceName, { action: taskAction, data: { - // TODO configuration fields + host: this.host, + lets_encrypt: this.isLetsEncryptEnabled, + activesync: this.isActivesyncEnabled, + dav: this.isDavEnabled, + mail_server: mail_server_tmp, + mail_domain: mail_domain_tmp, + ldap_domain: this.ldap_domain, + admin_users: this.admin_users.split("\n").join(",").trim(), + workers_count: this.workers_count.toString(), + auxiliary_account: this.isAuxiliaryAccountEnabled, }, extra: { - title: this.$t("settings.configure_instance", { + title: this.$t("settings.instance_configuration", { instance: this.instanceName, }), - description: this.$t("common.processing"), + description: this.$t("settings.configuring"), eventId, }, }) @@ -264,4 +499,11 @@ export default { \ No newline at end of file +.mg-bottom { + margin-bottom: $spacing-06; +} + +.maxwidth { + max-width: 38rem; +} + diff --git a/ui/yarn.lock b/ui/yarn.lock index 606ce02..d334598 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1012,10 +1012,10 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@nethserver/ns8-ui-lib@^0.1.22": - version "0.1.25" - resolved "https://registry.npmjs.org/@nethserver/ns8-ui-lib/-/ns8-ui-lib-0.1.25.tgz" - integrity sha512-jSYcG/PDd356Dvdb7BZkMBC+h9fxHvROWTrPsXRplCV4GsTcygO8Uc3H/ch3eo51Rre4qqrk/AP7j5Tzz3tBzQ== +"@nethserver/ns8-ui-lib@^0.1.27": + version "0.1.27" + resolved "https://registry.yarnpkg.com/@nethserver/ns8-ui-lib/-/ns8-ui-lib-0.1.27.tgz#3f699eff00588bd85cdf06af211def1bbea14601" + integrity sha512-tehnvePx10EezXe20u+UpOKjBnUmGwAIk5sXJ+vXg+b7L1knpnvOEadyyPRgpYGkGWeT/a1on5qzKRKDyBvRtw== dependencies: "@rollup/plugin-json" "^4.1.0" core-js "^3.15.2"