Sunil Mohan Adapa ae882fea70
email_server: Simplify domain configuration form
- By default, receive mail for all the domains on the system.

- Allow user to select a primary domain. This domain is used for TLS
certificate, automatically adding domain to sender address, etc.

- Don't expose postfix configuration parameters.

Tests:

- On installation, the domain list populated in postfix. Primary domain is
the one set in the config module. If it is not set, any other domain from
configured domains is taken.

- When not installed, adding/removing domains does not cause errors.

- Changing the domain in the domain view works. mydomain has the primary domain
set. myhostname has primary domain set. mydestination has default values and in
addition has all the domains on the system.

- /etc/mailname is populated with the primary domain.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2021-12-06 17:39:43 -05:00

199 lines
6.6 KiB
Python

"""FreedomBox email server app"""
# SPDX-License-Identifier: AGPL-3.0-or-later
import logging
from django.utils.translation import gettext_lazy as _
import plinth.app
import plinth.daemon
import plinth.frontpage
import plinth.menu
from plinth import actions
from plinth.modules.apache.components import Webserver
from plinth.modules.config import get_domainname
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.package import Packages, remove
from plinth.signals import domain_added, domain_removed
from . import audit, manifest
clamav_packages = ['clamav', 'clamav-daemon']
clamav_daemons = ['clamav-daemon', 'clamav-freshclam']
port_info = {
'postfix': ('smtp', 25, 'smtps', 465, 'smtp-submission', 587),
'dovecot': ('imaps', 993, 'pop3s', 995),
}
_description = [
_('<a href="/plinth/apps/roundcube/">Roundcube app</a> provides web '
'interface for users to access email.'),
_('During installation, any other email servers in the system will be '
'uninstalled.')
]
app = None
logger = logging.getLogger(__name__)
class EmailServerApp(plinth.app.App):
"""FreedomBox email server app"""
app_id = 'email_server'
app_name = _('Email Server')
_version = 1
def __init__(self):
"""The app's constructor"""
super().__init__()
self._add_ui_components()
# Other likely install conflicts have been discarded:
# - msmtp, nullmailer, sendmail don't cause install faults.
# - qmail and smail are missing in Bullseye (Not tested,
# but less likely due to that).
packages = Packages(
'packages-email-server', [
'postfix-ldap', 'postfix-sqlite', 'dovecot-pop3d',
'dovecot-imapd', 'dovecot-ldap', 'dovecot-lmtpd',
'dovecot-managesieved'
], conflicts=['exim4-base', 'exim4-config', 'exim4-daemon-light'],
conflicts_action=Packages.ConflictsAction.IGNORE)
self.add(packages)
packages = Packages('packages-email-server-skip-rec', ['rspamd'],
skip_recommends=True)
self.add(packages)
self._add_daemons()
self._add_firewall_ports()
# /rspamd location
webserver = Webserver(
'webserver-email', # unique id
'email-server-freedombox', # config file name
urls=['https://{host}/rspamd'])
self.add(webserver)
# Let's Encrypt event hook
letsencrypt = LetsEncrypt('letsencrypt-email-server',
domains=get_domains,
daemons=['postfix', 'dovecot'],
should_copy_certificates=False,
managing_app='email_server')
self.add(letsencrypt)
def _add_ui_components(self):
info = plinth.app.Info(
app_id=self.app_id, version=self._version, name=self.app_name,
short_description=_('Powered by Postfix, Dovecot & Rspamd'),
description=_description, manual_page='EmailServer',
clients=manifest.clients,
donation_url='https://freedomboxfoundation.org/donate/')
self.add(info)
menu_item = plinth.menu.Menu(
'menu_' + self.app_id, # unique id
info.name, # app display name
info.short_description, # app description
'roundcube', # icon name in `static/theme/icons/`
'email_server:index', # view name
parent_url_name='apps')
self.add(menu_item)
def _add_daemons(self):
for srvname in ['postfix', 'dovecot', 'rspamd']:
# Construct `listen_ports` parameter for the daemon
mixed = port_info.get(srvname, ())
port_numbers = [v for v in mixed if isinstance(v, int)]
listen = []
for n in port_numbers:
listen.append((n, 'tcp4'))
listen.append((n, 'tcp6'))
# Add daemon
daemon = plinth.daemon.Daemon('daemon-' + srvname, srvname,
listen_ports=listen)
self.add(daemon)
def _add_firewall_ports(self):
all_port_names = []
for mixed in port_info.values():
port_names = [v for v in mixed if isinstance(v, str)]
all_port_names.extend(port_names)
firewall = Firewall('firewall-email', self.app_name,
ports=all_port_names, is_external=True)
self.add(firewall)
@staticmethod
def post_init():
"""Perform post initialization operations."""
domain_added.connect(on_domain_added)
domain_removed.connect(on_domain_removed)
def diagnose(self):
"""Run diagnostics and return the results"""
results = super().diagnose()
results.extend([r.summarize() for r in audit.ldap.get()])
results.extend([r.summarize() for r in audit.spam.get()])
results.extend([r.summarize() for r in audit.tls.get()])
results.extend([r.summarize() for r in audit.rcube.get()])
return results
def get_domains():
"""Return the list of domains configured."""
default_domain = get_domainname()
return [default_domain] if default_domain else []
def setup(helper, old_version=None):
"""Installs and configures module"""
def _clear_conflicts():
component = app.get_component('packages-email-server')
packages_to_remove = component.find_conflicts()
if packages_to_remove:
logger.info('Removing conflicting packages: %s',
packages_to_remove)
remove(packages_to_remove)
# Install
helper.call('pre', _clear_conflicts)
app.setup(old_version)
# Setup
helper.call('post', audit.home.repair)
helper.call('post', audit.domain.set_domains)
helper.call('post', audit.ldap.repair)
helper.call('post', audit.spam.repair)
helper.call('post', audit.tls.repair)
helper.call('post', audit.rcube.repair)
# Reload
actions.superuser_run('service', ['reload', 'postfix'])
actions.superuser_run('service', ['reload', 'dovecot'])
actions.superuser_run('service', ['reload', 'rspamd'])
# Expose to public internet
helper.call('post', app.enable)
def on_domain_added(sender, domain_type, name, description='', services=None,
**kwargs):
"""Handle addition of a new domain."""
if app.needs_setup():
return
audit.domain.set_domains()
def on_domain_removed(sender, domain_type, name, **kwargs):
"""Handle removal of a domain."""
if app.needs_setup():
return
audit.domain.set_domains()