# SPDX-License-Identifier: AGPL-3.0-or-later """FreedomBox app to manage an email server.""" import logging from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ import plinth.app from plinth import cfg, frontpage, menu from plinth.config import DropinConfigs from plinth.daemon import Daemon from plinth.modules.apache.components import Webserver from plinth.modules.backups.components import BackupRestore from plinth.modules.config import get_domainname from plinth.modules.firewall.components import (Firewall, FirewallLocalProtection) from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.package import Packages from plinth.privileged import service as service_privileged from plinth.signals import domain_added, domain_removed from plinth.utils import format_lazy from . import aliases, manifest, privileged _description = [ _('This is a complete email server solution using Postfix, Dovecot, ' 'and Rspamd. Postfix sends and receives emails. Dovecot allows ' 'email clients to access your mailbox using IMAP and POP3. Rspamd deals ' 'with spam.'), _('Email server currently does not work with many free domain services ' 'including those provided by the FreedomBox Foundation. Many ISPs ' 'also restrict outgoing email. Some lift the restriction after an ' 'explicit request. See manual page for more information.'), format_lazy( _('Each user on {box_name} gets an email address like ' 'user@mydomain.example. They will also receive mail from all ' 'addresses that look like user+foo@mydomain.example. Further, ' 'they can add aliases to their email address. Necessary aliases ' 'such as "postmaster" are automatically created pointing to the ' 'first admin user.'), box_name=_(cfg.box_name)), _('Roundcube app provides web ' 'interface for users to access email.'), _('During installation, any other email servers in the system will be ' 'uninstalled.') ] logger = logging.getLogger(__name__) class EmailApp(plinth.app.App): """FreedomBox app for an email server.""" app_id = 'email' _version = 4 def __init__(self) -> None: """Initialize the email app.""" super().__init__() info = plinth.app.Info(app_id=self.app_id, version=self._version, name=_('Postfix/Dovecot'), icon_filename='email', short_description=_('Email Server'), description=_description, manual_page='Email', clients=manifest.clients, donation_url='https://rspamd.com/support.html') self.add(info) menu_item = menu.Menu('menu-email', info.name, info.short_description, info.icon_filename, 'email:index', parent_url_name='apps') self.add(menu_item) shortcut = frontpage.Shortcut( 'shortcut-email', info.name, short_description=info.short_description, icon=info.icon_filename, description=info.description, manual_page=info.manual_page, configure_url=reverse_lazy('email:index'), clients=info.clients, login_required=True) self.add(shortcut) shortcut = frontpage.Shortcut( 'shortcut-email-aliases', _('My Email Aliases'), short_description=_('Manage Aliases for Mailbox'), icon=info.icon_filename, url=reverse_lazy('email:aliases'), login_required=True) self.add(shortcut) # 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', [ 'postfix', 'postfix-sqlite', 'dovecot-pop3d', 'dovecot-imapd', 'dovecot-lmtpd', 'dovecot-managesieved', 'dovecot-ldap', 'rspamd', 'redis-server', 'openssl' ], conflicts=['exim4-base', 'exim4-config', 'exim4-daemon-light'], conflicts_action=Packages.ConflictsAction.REMOVE) self.add(packages) dropin_configs = DropinConfigs('dropin-configs-email', [ '/etc/apache2/conf-available/email-freedombox.conf', '/etc/dovecot/conf.d/05-freedombox-passdb.conf', '/etc/dovecot/conf.d/05-freedombox-userdb.conf', '/etc/dovecot/conf.d/15-freedombox-auth.conf', '/etc/dovecot/conf.d/15-freedombox-mail.conf', '/etc/dovecot/conf.d/90-freedombox-lmtp.conf', '/etc/dovecot/conf.d/90-freedombox-mailboxes.conf', '/etc/dovecot/conf.d/90-freedombox-master.conf', '/etc/dovecot/conf.d/90-freedombox-tls.conf', '/etc/dovecot/conf.d/95-freedombox-sieve.conf', '/etc/dovecot/conf.d/freedombox-ldap.conf.ext', '/etc/fail2ban/jail.d/dovecot-freedombox.conf', '/etc/postfix/freedombox-aliases.cf', '/etc/rspamd/local.d/freedombox-logging.inc', '/etc/rspamd/local.d/freedombox-milter-headers.conf', '/etc/rspamd/local.d/freedombox-redis.conf', ]) self.add(dropin_configs) dropin_configs_sieve = DropinConfigs( 'dropin-configs-email-sieve', ['/etc/dovecot/freedombox-sieve-after/sort-spam.sieve']) self.add(dropin_configs_sieve) listen_ports = [(25, 'tcp4'), (25, 'tcp6'), (465, 'tcp4'), (465, 'tcp6'), (587, 'tcp4'), (587, 'tcp6')] daemon = Daemon('daemon-email-postfix', 'postfix', listen_ports=listen_ports) self.add(daemon) listen_ports = [(143, 'tcp4'), (143, 'tcp6'), (993, 'tcp4'), (993, 'tcp6'), (110, 'tcp4'), (110, 'tcp6'), (995, 'tcp4'), (995, 'tcp6'), (4190, 'tcp4'), (4190, 'tcp6')] daemon = Daemon('daemon-email-dovecot', 'dovecot', listen_ports=listen_ports) self.add(daemon) listen_ports = [(11332, 'tcp4'), (11332, 'tcp6'), (11333, 'tcp4'), (11333, 'tcp6'), (11334, 'tcp4'), (11334, 'tcp6')] daemon = Daemon('daemon-email-rspamd', 'rspamd', listen_ports=listen_ports) self.add(daemon) daemon = Daemon('daemon-email-redis', 'redis-server', listen_ports=[(6379, 'tcp4'), (6379, 'tcp6')]) self.add(daemon) port_names = [ 'smtp', 'smtps', 'smtp-submission', 'imaps', 'pop3s', 'managesieve' ] firewall = Firewall('firewall-email', info.name, ports=port_names, is_external=True) self.add(firewall) firewall_local_protection = FirewallLocalProtection( 'firewall-local-protection-email', ['11334']) self.add(firewall_local_protection) # /rspamd location webserver = Webserver( 'webserver-email', # unique id 'email-freedombox', # config file name urls=['https://{host}/rspamd']) self.add(webserver) # Let's Encrypt event hook letsencrypt = LetsEncrypt( 'letsencrypt-email-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') self.add(letsencrypt) letsencrypt = LetsEncrypt( 'letsencrypt-email-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') self.add(letsencrypt) backup_restore = BackupRestore('backup-restore-email', **manifest.backup) self.add(backup_restore) @staticmethod def post_init(): """Perform post initialization operations.""" domain_added.connect(on_domain_added) domain_removed.connect(on_domain_removed) def setup(self, old_version): """Install and configure the app.""" # Install super().setup(old_version) # Setup privileged.setup_home() self.get_component('letsencrypt-email-postfix').setup_certificates() self.get_component('letsencrypt-email-dovecot').setup_certificates() privileged.domain.set_all_domains() aliases.first_setup() privileged.setup_postfix() aliases.setup_common_aliases(_get_first_admin()) # Enable drop-in configuration files component for sieve (temporarily) # to ensure that sievec can compile. self.get_component('dropin-configs-email-sieve').enable() privileged.setup_spam() # Restart daemons service_privileged.try_restart('postfix') service_privileged.try_restart('dovecot') service_privileged.try_restart('rspamd') # Expose to public internet if old_version == 0: self.enable() def get_domains(): """Return the list of domains configured.""" default_domain = get_domainname() return [default_domain] if default_domain else [] def _get_first_admin(): """Return an admin user in the system or None if non exist.""" from django.contrib.auth.models import User users = User.objects.filter(groups__name='admin') return users[0].username if users else None def on_domain_added(sender, domain_type, name, description='', services=None, **kwargs): """Handle addition of a new domain.""" app = plinth.app.App.get('email') if app.needs_setup(): return privileged.domain.set_all_domains() def on_domain_removed(sender, domain_type, name='', **kwargs): """Handle removal of a domain.""" app = plinth.app.App.get('email') if app.needs_setup(): return privileged.domain.set_all_domains()