"""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 = [ _('Roundcube app 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', '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-postfix', domains='*', daemons=['postfix'], should_copy_certificates=True, private_key_path='/etc/postfix/letsencrypt/{domain}/chain.pem', certificate_path='/etc/postfix/letsencrypt/{domain}/chain.pem', user_owner='root', group_owner='root', managing_app='email_server') self.add(letsencrypt) letsencrypt = LetsEncrypt( 'letsencrypt-email-server-dovecot', domains='*', daemons=['dovecot'], should_copy_certificates=True, private_key_path='/etc/dovecot/letsencrypt/{domain}/privkey.pem', certificate_path='/etc/dovecot/letsencrypt/{domain}/cert.pem', user_owner='root', group_owner='root', 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()]) 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) app.get_component('letsencrypt-email-server-postfix').setup_certificates() app.get_component('letsencrypt-email-server-dovecot').setup_certificates() helper.call('post', audit.domain.set_domains) helper.call('post', audit.ldap.repair) helper.call('post', audit.spam.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()