mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
sogo: Add a new app for SOGo groupware
SOGo is an open source webmail client and groupware available in Debian.
Make a new FreedomBox app for it to be used with the local
Postfix/Dovecot email server.
SOGo requires a database to store events/tasks as well as user settings. Memcached
is also required for caching.
Users log in with their username (as opposed to username + domain on
Roundcube). The host header seen from the first login will be
associated with the user profile. So, if a user logs into SOGo from
freedombox.local and later configures the email server to use
example.com, they will manually have to edit their account(s) to show
the updated domain.
Authentication is done via openldap. It is possible to authenticate with
apache, however it is limited to http basic auth, so mod_auth_tkt cannot
be used. See: b40d777a86/SoObjects/SOGo/SOGoProxyAuthenticator.m (L137)
Configuring http basic auth in my opinion wouldn't add much to
the user experience. It would actually take away the usage of SOGO's built
in TOTP feature.
SOGo only accepts configurations from /etc/sogo/sogo.conf, other configs
from sogo.d don't get recognised.
Use the sogo icon from upstream source. Update sogo.png and sogo.svg to be the
same image (but resized) that is provided in the upstream source. The previous
image was download from Wikimedia Commons.
Update smtp settings so that messages can be sent
Test result for mail deliverability sent with SOGo: https://www.mail-tester.com/test-pdf2yzy6n
The result shows that the message is not DKIM signed. This seems to be
an issue not specific to SOGo. Mails sent from Thunderbird don't get
signed either.
Tests:
- Install app and log in with a FreedomBox user. Create a new event
titled "Lunch with 🍕 and fries". Confirm the pizza character displays properly.
- Backup the app and uninstall it.
- Restore from the backup, log in and confirm the event gets restored.
To-do:
- test ActiveSync
- create a fail2ban jail
- include the icons in the copyright file
- test sending email in a production setup
- test sieve filters
- write tests
https://salsa.debian.org/freedombox-team/freedombox/-/issues/56
[Sunil]
- App:
- Update icons to be uniform size as all other apps and copyright information.
- Since SOGo is not configured to trust the authentication from Apache, it
does not require FirewallLocalProtection. Remove it.
- Expand app description. Talk about Email Server app.
- Update to match recent tags related changes.
- Make memcached a shared daemon as other processes might use it.
- Added shared daemon for PostgreSQL.
- Don't start services when rerunning setup if the app is currently disabled.
- Don't restart memcached during a restore operation.
- Security:
- Add system security restrictions to the daemon.
- Don't use fail2ban jail. SOGo has a mechanism to lock users for a few
minutes. Use that instead.
- Apache:
- Make /.well-known URLs work by moving their definitions to global section.
- Remove old (<2.4) Apache authorization keywords.
- Simplify, indentation, new line at EOF.
- Manifest:
- Add more tags.
- Add SOGo connector, DAVx5, and GNOME Calendar to list of clients.
- Add 'sogo' to list of service to bring down during backup/restore.
- Privileged:
- Switch from MySQL to PostgreSQL as it is recommended by SOGo.
- Use existing utility to generate database password.
- Use plget and plmerge utilities from gnustep-common package to parse/edit
the configuration instead of augeas which don't have a dedicated lens.
- Don't reset the domain when rerunning setup.
- Ensure that the configuration file has proper ownership and permissions even
when it did not exist previously.
- Add typing information for most methods.
- Remove configuration file after uninstall.
- Configuration:
- Define database URLs for all seven database tables.
- Set calendar default roles as suggested in the installation guide.
- Refresh view automatically every minute to check for new mail.
- Use the mechanism to lock account after failed login attempts.
- Add folder name for Junk folder too explicitly.
- Tests: Add basic functional tests.
Tests:
- Functional tests work.
- Rerunning setup does not change the domain back to the primary domain of the
email server.
- Login works. Sending mail and reading mail works. Creating calendar events and
contact works.
- Changing the domain sets the domain value properly in the configuration file.
Configured domain is shown properly on the form.
- Backup and restore work as expected.
- When configuration file is removed and setup is re-run, then the file is
created with proper ownership and permissions.
- 'systemd-analyze security sogo.service' shows a good score.
Signed-off-by: Benedek Nagy <contact@nbenedek.me>
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
74e908ea82
commit
6887c960fe
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -275,6 +275,12 @@ Copyright: Interface (https://www.shareicon.net/author/interface)
|
||||
Comment: https://www.shareicon.net/universal-interface-interface-sharing-share-697502
|
||||
License: CC-BY-3.0
|
||||
|
||||
Files: plinth/modules/sogo/static/icons/sogo.png
|
||||
plinth/modules/sogo/static/icons/sogo.svg
|
||||
Copyright: 2024 Inverse inc./Alinto
|
||||
Comment: https://github.com/Alinto/sogo/blob/master/COPYING.GPL
|
||||
License: GPL-2
|
||||
|
||||
Files: plinth/modules/syncthing/static/icons/syncthing.png
|
||||
Copyright: 2015 Jack Palevich <jack.palevich@gmail.com>
|
||||
2014 The Syncthing Authors
|
||||
|
||||
121
plinth/modules/sogo/__init__.py
Normal file
121
plinth/modules/sogo/__init__.py
Normal file
@ -0,0 +1,121 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to configure SOGo."""
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, frontpage, menu
|
||||
from plinth.config import DropinConfigs
|
||||
from plinth.daemon import Daemon, SharedDaemon
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
from plinth.privileged import service as service_privileged
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import manifest, privileged
|
||||
|
||||
_description = [
|
||||
_('SOGo is a groupware server that provides a rich web interface for '
|
||||
'email, calendar, tasks, and contacts. Calendar, tasks, and contacts '
|
||||
'can also be accessed with various mobile and desktop applications '
|
||||
'using the CalDAV and CardDAV standards.'),
|
||||
format_lazy(
|
||||
_('Webmail works with the <a href="{email_url}">Postfix/Dovecot</a> '
|
||||
'email server app to retrieve, manage, and send email.'),
|
||||
email_url=reverse_lazy('email:index')),
|
||||
format_lazy(
|
||||
_('All users on {box_name} can login into and use SOGo. Mails '
|
||||
'delivered to their mailboxes by the email server app can be read '
|
||||
'and new mail can be sent out.'), box_name=_(cfg.box_name)),
|
||||
]
|
||||
|
||||
|
||||
class SOGoApp(app_module.App):
|
||||
"""FreedomBox app for SOGo."""
|
||||
|
||||
app_id = 'sogo'
|
||||
|
||||
_version = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=self._version,
|
||||
depends=['email'], name=_('SOGo'),
|
||||
icon_filename='sogo', description=_description,
|
||||
manual_page='SOGo', clients=manifest.clients,
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-sogo', info.name, info.icon_filename,
|
||||
info.tags, 'sogo:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-sogo', info.name,
|
||||
icon=info.icon_filename, url='/SOGo/',
|
||||
clients=info.clients, tags=info.tags)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-sogo',
|
||||
['sogo', 'postgresql', 'memcached'])
|
||||
self.add(packages)
|
||||
|
||||
dropin_configs = DropinConfigs('dropin-configs-sogo', [
|
||||
'/etc/apache2/conf-available/sogo-freedombox.conf',
|
||||
])
|
||||
self.add(dropin_configs)
|
||||
|
||||
firewall = Firewall('firewall-sogo', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
webserver = Webserver('webserver-sogo', 'sogo-freedombox',
|
||||
urls=['https://{host}/SOGo/'])
|
||||
self.add(webserver)
|
||||
|
||||
daemon1 = SharedDaemon('shared-daemon-sogo-memcached', 'memcached',
|
||||
listen_ports=[(11211, 'tcp4')])
|
||||
self.add(daemon1)
|
||||
|
||||
daemon2 = SharedDaemon('shared-daemon-sogo-postgresql', 'postgresql')
|
||||
self.add(daemon2)
|
||||
|
||||
daemon3 = Daemon('daemon-sogo', 'sogo', listen_ports=[(20000, 'tcp4')])
|
||||
self.add(daemon3)
|
||||
|
||||
backup_restore = SOGoBackupRestore('backup-restore-sogo',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
def setup(self, old_version):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
privileged.setup()
|
||||
service_privileged.try_restart('sogo')
|
||||
service_privileged.try_restart('memcached')
|
||||
|
||||
if not old_version:
|
||||
self.enable()
|
||||
|
||||
def uninstall(self):
|
||||
"""De-configure and uninstall the app."""
|
||||
super().uninstall()
|
||||
privileged.uninstall()
|
||||
|
||||
|
||||
class SOGoBackupRestore(BackupRestore):
|
||||
"""Component to backup/restore SOGo."""
|
||||
|
||||
def backup_pre(self, packet):
|
||||
"""Save database contents."""
|
||||
super().backup_pre(packet)
|
||||
privileged.dump_database()
|
||||
|
||||
def restore_post(self, packet):
|
||||
"""Restore database contents."""
|
||||
super().restore_post(packet)
|
||||
privileged.restore_database()
|
||||
@ -0,0 +1,28 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
[Service]
|
||||
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_NET_ADMIN CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_KILL CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_PACCT CAP_SYS_TTY_CONFIG CAP_SYS_BOOT CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_BPF
|
||||
DevicePolicy=closed
|
||||
LockPersonality=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateMounts=yes
|
||||
PrivateTmp=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHome=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/spool/sogo /var/log/sogo
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
RestrictNamespaces=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictRealtime=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@resources
|
||||
SystemCallFilter=~@privileged
|
||||
SystemCallErrorNumber=EPERM
|
||||
@ -0,0 +1,36 @@
|
||||
##
|
||||
## On all sites, provide SOGo on a default path: /SOGo
|
||||
## https://www.sogo.nu/support/faq/how-to-configure-apache-as-frontend.html
|
||||
##
|
||||
|
||||
Alias /SOGo.woa/WebServerResources/ /usr/lib/GNUstep/SOGo/WebServerResources/
|
||||
Alias /SOGo/WebServerResources/ /usr/lib/GNUstep/SOGo/WebServerResources/
|
||||
|
||||
Redirect 301 /.well-known/caldav /SOGo/dav
|
||||
Redirect 301 /.well-known/carddav /SOGo/dav
|
||||
|
||||
<Location /SOGo>
|
||||
ProxyPass http://127.0.0.1:20000/SOGo retry=0 nocanon
|
||||
ProxyPreserveHost On
|
||||
SetEnv proxy-nokeepalive 1
|
||||
|
||||
<IfModule 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
|
||||
</IfModule>
|
||||
|
||||
AddDefaultCharset UTF-8
|
||||
</Location>
|
||||
|
||||
<Directory /usr/lib/GNUstep/SOGo/>
|
||||
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.
|
||||
<IfModule expires_module>
|
||||
ExpiresActive On
|
||||
ExpiresDefault "access plus 1 year"
|
||||
</IfModule>
|
||||
</Directory>
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.sogo
|
||||
23
plinth/modules/sogo/forms.py
Normal file
23
plinth/modules/sogo/forms.py
Normal file
@ -0,0 +1,23 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Forms for the SOGo app."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.modules.names.components import DomainName
|
||||
|
||||
|
||||
def _get_domain_choices():
|
||||
"""Double domain entries for inclusion in the choice field."""
|
||||
return ((domain.name, domain.name) for domain in DomainName.list())
|
||||
|
||||
|
||||
class DomainForm(forms.Form):
|
||||
domain = forms.ChoiceField(
|
||||
choices=_get_domain_choices,
|
||||
label=_('Domain'),
|
||||
help_text=_(
|
||||
'Mails are received for all domains configured in the system. '
|
||||
'Among these, select the most important one.'),
|
||||
required=True,
|
||||
)
|
||||
77
plinth/modules/sogo/manifest.py
Normal file
77
plinth/modules/sogo/manifest.py
Normal file
@ -0,0 +1,77 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.clients import store_url
|
||||
|
||||
from . import privileged
|
||||
|
||||
clients = [
|
||||
{
|
||||
'name': _('SOGo'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/SOGo/'
|
||||
}]
|
||||
},
|
||||
{
|
||||
'name':
|
||||
_('Thunderbird + SOGo connector'),
|
||||
'platforms': [{
|
||||
'type': 'download',
|
||||
'os': 'gnu-linux',
|
||||
'url': 'https://www.sogo.nu/download.html#/frontends'
|
||||
}, {
|
||||
'type': 'download',
|
||||
'os': 'macos',
|
||||
'url': 'https://www.sogo.nu/download.html#/frontends'
|
||||
}, {
|
||||
'type': 'download',
|
||||
'os': 'windows',
|
||||
'url': 'https://www.sogo.nu/download.html#/frontends'
|
||||
}]
|
||||
},
|
||||
{
|
||||
'name':
|
||||
_('DAVx5'),
|
||||
'platforms': [{
|
||||
'type': 'store',
|
||||
'os': 'android',
|
||||
'store_name': 'f-droid',
|
||||
'url': store_url('f-droid', 'at.bitfire.davdroid'),
|
||||
}, {
|
||||
'type': 'store',
|
||||
'os': 'android',
|
||||
'store_name': 'google-play',
|
||||
'url': store_url('google-play', 'at.bitfire.davdroid'),
|
||||
}]
|
||||
},
|
||||
{
|
||||
'name':
|
||||
_('GNOME Calendar'),
|
||||
'platforms': [{
|
||||
'type': 'package',
|
||||
'format': 'deb',
|
||||
'name': 'gnome-calendar'
|
||||
}]
|
||||
},
|
||||
]
|
||||
|
||||
backup = {
|
||||
'data': {
|
||||
'files': [str(privileged.DB_BACKUP_FILE)],
|
||||
},
|
||||
'services': ['sogo'],
|
||||
'secrets': {
|
||||
'directories': [str(privileged.CONFIG_FILE)]
|
||||
},
|
||||
}
|
||||
|
||||
tags = [
|
||||
_('Webmail'),
|
||||
_('Groupware'),
|
||||
_('Calender'),
|
||||
_('Address book'),
|
||||
_('CalDAV'),
|
||||
_('CardDAV')
|
||||
]
|
||||
161
plinth/modules/sogo/privileged.py
Normal file
161
plinth/modules/sogo/privileged.py
Normal file
@ -0,0 +1,161 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Configure SOGo."""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from plinth import utils
|
||||
from plinth.actions import privileged
|
||||
from plinth.db import postgres
|
||||
from plinth.modules.email.privileged.domain import \
|
||||
get_domains as get_email_domains
|
||||
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'sogo_fbx'
|
||||
DB_USER = 'sogo_fbx'
|
||||
SERVICE_NAME = 'sogo'
|
||||
|
||||
DB_BACKUP_FILE = pathlib.Path('/var/lib/plinth/backups-data/sogo-database.sql')
|
||||
CONFIG_FILE = pathlib.Path('/etc/sogo/sogo.conf')
|
||||
|
||||
|
||||
@privileged
|
||||
def setup() -> None:
|
||||
"""Setup SOGo database and configuration."""
|
||||
database_password = utils.generate_password(16)
|
||||
postgres.create_database(DB_NAME, DB_USER, database_password)
|
||||
_create_config(database_password)
|
||||
|
||||
|
||||
def _create_config(db_password: str):
|
||||
"""Configure /etc/sogo/sogo.conf"""
|
||||
try:
|
||||
domain = _get_config_value('SOGoMailDomain')
|
||||
if not domain:
|
||||
# Try to get the domain configured for the email app
|
||||
domain = get_email_domains()['primary_domain']
|
||||
except FileNotFoundError:
|
||||
domain = 'localhost'
|
||||
|
||||
connection = f'postgresql://{DB_USER}:{db_password}@{DB_HOST}/{DB_NAME}'
|
||||
|
||||
content = f'''
|
||||
{{
|
||||
/* General */
|
||||
SOGoMailDomain = "{domain}";
|
||||
SOGoLanguage = "English";
|
||||
SOGoTimeZone = "UTC";
|
||||
SOGoCalendarDefaultRoles = ("PublicViewer", "ConfidentialDAndTViewer");
|
||||
SOGoAppointmentSendEMailNotifications = YES;
|
||||
SOGoRefreshViewCheck = "every_minute";
|
||||
|
||||
/* Authentication */
|
||||
SOGoMaximumFailedLoginCount = "10";
|
||||
SOGoMaximumFailedLoginInterval = "300";
|
||||
SOGoFailedLoginBlockInterval = "300";
|
||||
|
||||
/* Database */
|
||||
SOGoProfileURL = "{connection}/sogo_user_profile";
|
||||
OCSFolderInfoURL = "{connection}/sogo_folder_info";
|
||||
OCSSessionsFolderURL = "{connection}/sogo_sessions_folder";
|
||||
OCSEMailAlarmsFolderURL = "{connection}/sogo_alarms_folder";
|
||||
OCSStoreURL = "{connection}/sogo_store";
|
||||
OCSAclURL = "{connection}/sogo_acl";
|
||||
OCSCacheFolderURL = "{connection}/sogo_cache_folder";
|
||||
OCSAdminURL = "{connection}/sogo_admin";
|
||||
|
||||
/* Cache */
|
||||
SOGoMemcachedHost = "127.0.0.1";
|
||||
|
||||
/* SMTP */
|
||||
SOGoMailingMechanism = "smtp";
|
||||
SOGoSMTPServer = "smtp://127.0.0.1:587/?tls=YES&tlsVerifyMode=allowInsecureLocalhost";
|
||||
SOGoSMTPAuthenticationType = "PLAIN";
|
||||
|
||||
/* IMAP */
|
||||
SOGoDraftsFolderName = "Drafts";
|
||||
SOGoSentFolderName = "Sent";
|
||||
SOGoTrashFolderName = "Trash";
|
||||
SOGoJunkFolderName = "Junk";
|
||||
SOGoIMAPServer = "imap://127.0.0.1:143/?tls=YES&tlsVerifyMode=allowInsecureLocalhost";
|
||||
SOGoSieveServer = "sieve://127.0.0.14190/?tls=YES&tlsVerifyMode=allowInsecureLocalhost";
|
||||
|
||||
/* LDAP */
|
||||
SOGoUserSources = ({{
|
||||
type = "ldap";
|
||||
CNFieldName = "cn";
|
||||
IDFieldName = "uid";
|
||||
UIDFieldName = "uid";
|
||||
baseDN = "ou=users,dc=thisbox";
|
||||
canAuthenticate = YES;
|
||||
displayName = "Shared Addresses";
|
||||
hostname = "ldap://127.0.0.1:389";
|
||||
id = "directory";
|
||||
isAddressBook = YES;
|
||||
}});
|
||||
}}''' # noqa: E501
|
||||
CONFIG_FILE.touch(0o640, exist_ok=True) # In case the file does not exist
|
||||
CONFIG_FILE.chmod(0o640) # In case the file pre-existed
|
||||
shutil.chown(CONFIG_FILE, 'root', 'sogo')
|
||||
CONFIG_FILE.write_text(content, encoding='utf-8')
|
||||
|
||||
|
||||
@privileged
|
||||
def dump_database() -> None:
|
||||
"""Dump database to file."""
|
||||
postgres.dump_database(DB_BACKUP_FILE, DB_NAME)
|
||||
|
||||
|
||||
@privileged
|
||||
def restore_database() -> None:
|
||||
"""Restore database from file."""
|
||||
password = _read_db_password()
|
||||
postgres.restore_database(DB_BACKUP_FILE, DB_NAME, DB_USER, password)
|
||||
|
||||
|
||||
def _read_db_password() -> str:
|
||||
"""Extract the database password from /etc/sogo/sogo.conf using regex"""
|
||||
pattern = r'postgresql://[^:]+:([^@]+)@localhost'
|
||||
match = re.search(pattern, _get_config_value('SOGoProfileURL'))
|
||||
if not match:
|
||||
raise ValueError('Could not extract password')
|
||||
|
||||
return match.group(1)
|
||||
|
||||
|
||||
@privileged
|
||||
def get_domain() -> str:
|
||||
"""Get the value of SOGoMailDomain from /etc/sogo/sogo.conf"""
|
||||
return _get_config_value('SOGoMailDomain')
|
||||
|
||||
|
||||
@privileged
|
||||
def set_domain(domain: str):
|
||||
"""Set the value of SOGoMailDomain in /etc/sogo/sogo.conf"""
|
||||
_set_config_value('SOGoMailDomain', domain)
|
||||
|
||||
|
||||
def _get_config_value(key: str) -> str:
|
||||
"""Return the value of a property from the configuration file."""
|
||||
process = subprocess.run(['plget', key], input=CONFIG_FILE.read_bytes(),
|
||||
stdout=subprocess.PIPE, check=True)
|
||||
return process.stdout.decode().strip()
|
||||
|
||||
|
||||
def _set_config_value(key: str, value: str):
|
||||
"""Set the value of a property in the configuration file."""
|
||||
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
||||
temp_file.write(f'{{\n{key} = "{value}";\n}}'.encode('utf-8'))
|
||||
temp_file.close()
|
||||
subprocess.run(['plmerge', CONFIG_FILE, temp_file.name], check=True)
|
||||
pathlib.Path(temp_file.name).unlink()
|
||||
|
||||
|
||||
@privileged
|
||||
def uninstall() -> None:
|
||||
"""Uninstall SOGo: drop database and configuration files."""
|
||||
postgres.drop_database(DB_NAME, DB_USER)
|
||||
CONFIG_FILE.unlink(missing_ok=True)
|
||||
BIN
plinth/modules/sogo/static/icons/sogo.png
Normal file
BIN
plinth/modules/sogo/static/icons/sogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
46
plinth/modules/sogo/static/icons/sogo.svg
Normal file
46
plinth/modules/sogo/static/icons/sogo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.3 KiB |
0
plinth/modules/sogo/tests/__init__.py
Normal file
0
plinth/modules/sogo/tests/__init__.py
Normal file
42
plinth/modules/sogo/tests/test_functional.py
Normal file
42
plinth/modules/sogo/tests/test_functional.py
Normal file
@ -0,0 +1,42 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Functional, browser based tests for SOGo app."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.tests import functional
|
||||
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.sogo]
|
||||
|
||||
|
||||
class TestSOGoApp(functional.BaseAppTests):
|
||||
"""Basic tests for the SOGo app."""
|
||||
app_name = 'sogo'
|
||||
has_service = True
|
||||
disable_after_tests = False
|
||||
|
||||
def test_login(self, session_browser):
|
||||
"""Test that login to SOGo interface works."""
|
||||
_login(session_browser)
|
||||
assert functional.eventually(_is_logged_in, [session_browser])
|
||||
|
||||
|
||||
def _login(browser) -> None:
|
||||
"""Login to SOGo web interface."""
|
||||
functional.visit(browser, '/SOGo/')
|
||||
username = functional.config['DEFAULT']['username']
|
||||
password = functional.config['DEFAULT']['password']
|
||||
functional.eventually(browser.find_by_id, ['input_1'])
|
||||
time.sleep(1) # For some reason, waiting does not work
|
||||
browser.find_by_id('input_1').fill(username)
|
||||
browser.find_by_id('passwordField').fill(password)
|
||||
submit = browser.find_by_css(
|
||||
'form[name=loginForm] button[type=submit]').first
|
||||
functional.submit(browser, element=submit)
|
||||
|
||||
|
||||
def _is_logged_in(browser) -> bool:
|
||||
"""Return whether SOGo login was successful."""
|
||||
logout = browser.find_by_css('a[href="../logoff"]')
|
||||
return bool(logout)
|
||||
10
plinth/modules/sogo/urls.py
Normal file
10
plinth/modules/sogo/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""URLs for the SOGo module."""
|
||||
|
||||
from django.urls import re_path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^apps/sogo/$', views.SOGoAppView.as_view(), name='index')
|
||||
]
|
||||
34
plinth/modules/sogo/views.py
Normal file
34
plinth/modules/sogo/views.py
Normal file
@ -0,0 +1,34 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Views for the SOGo app."""
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.privileged import service as service_privileged
|
||||
from plinth.views import AppView
|
||||
|
||||
from . import forms, privileged
|
||||
|
||||
|
||||
class SOGoAppView(AppView):
|
||||
"""Server configuration page."""
|
||||
app_id = 'sogo'
|
||||
form_class = forms.DomainForm
|
||||
|
||||
def get_initial(self):
|
||||
"""Return the initial values to populate in the form."""
|
||||
initial = super().get_initial()
|
||||
initial['domain'] = privileged.get_domain()
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Update the settings for changed domain values."""
|
||||
old_data = form.initial
|
||||
new_data = form.cleaned_data
|
||||
if old_data['domain'] != new_data['domain']:
|
||||
privileged.set_domain(new_data['domain'])
|
||||
service_privileged.try_restart('sogo')
|
||||
service_privileged.try_restart('memcached')
|
||||
messages.success(self.request, _('Configuration updated'))
|
||||
|
||||
return super().form_valid(form)
|
||||
@ -53,6 +53,7 @@ _site_url = {
|
||||
'syncthing': '/syncthing/',
|
||||
'rssbridge': '/rss-bridge/',
|
||||
'ttrss': '/tt-rss/',
|
||||
'sogo': '/SOGo/',
|
||||
}
|
||||
|
||||
_sys_modules = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user