mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
- Since we have important fixes deployed that we would like to get effected immediately. Tests: - Not tested. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
277 lines
11 KiB
Python
277 lines
11 KiB
Python
# 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.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, gettext_noop
|
|
|
|
from . import aliases, dovecot, 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)),
|
|
_('<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.')
|
|
]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailApp(plinth.app.App):
|
|
"""FreedomBox app for an email server."""
|
|
|
|
app_id = 'email'
|
|
|
|
_version = 9
|
|
|
|
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', description=_description,
|
|
manual_page='Email', clients=manifest.clients,
|
|
tags=manifest.tags,
|
|
donation_url='https://rspamd.com/support.html')
|
|
self.add(info)
|
|
|
|
menu_item = menu.Menu('menu-email', info.name, info.icon_filename,
|
|
info.tags, 'email:index', parent_url_name='apps')
|
|
self.add(menu_item)
|
|
|
|
shortcut = frontpage.Shortcut(
|
|
'shortcut-email', info.name, icon=info.icon_filename,
|
|
description=info.description, manual_page=info.manual_page,
|
|
configure_url=reverse_lazy('email:index'), clients=info.clients,
|
|
tags=info.tags, login_required=True)
|
|
self.add(shortcut)
|
|
|
|
tags = [gettext_noop('More emails'), gettext_noop('Same mailbox')]
|
|
shortcut = frontpage.Shortcut('shortcut-email-aliases',
|
|
_('My Email Aliases'),
|
|
icon=info.icon_filename, tags=tags,
|
|
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',
|
|
'dovecot-fts-xapian', 'rspamd', 'redis-server', 'openssl'
|
|
], conflicts=['exim4-base', 'exim4-config', 'exim4-daemon-light'],
|
|
conflicts_action=Packages.ConflictsAction.REMOVE,
|
|
rerun_setup_on_upgrade=True)
|
|
self.add(packages)
|
|
|
|
dropin_configs = DropinConfigs('dropin-configs-email', [
|
|
'/etc/apache2/conf-available/email-freedombox.conf',
|
|
'/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',
|
|
'/etc/rspamd/local.d/freedombox-dkim-signing.conf'
|
|
])
|
|
self.add(dropin_configs)
|
|
dropin_configs_sieve = DropinConfigs('dropin-configs-email-sieve', [
|
|
'/etc/dovecot/freedombox-sieve/learn-ham.sieve',
|
|
'/etc/dovecot/freedombox-sieve/learn-spam.sieve',
|
|
'/etc/dovecot/freedombox-sieve-after/sort-spam.sieve'
|
|
])
|
|
self.add(dropin_configs_sieve)
|
|
dropin_configs_dovecot = DovecotDropinConfigs(
|
|
'dropin-configs-email-dovecot', [
|
|
'/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-imap.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/95-freedombox-fts.conf',
|
|
'/etc/dovecot/conf.d/freedombox-ldap.conf.ext'
|
|
])
|
|
self.add(dropin_configs_dovecot)
|
|
|
|
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()
|
|
self.get_component('dropin-configs-email-dovecot').enable()
|
|
service_privileged.try_restart('dovecot')
|
|
privileged.setup_spam()
|
|
|
|
# Restart daemons
|
|
if self.is_enabled():
|
|
service_privileged.restart('postfix')
|
|
service_privileged.restart('dovecot')
|
|
service_privileged.restart('rspamd')
|
|
|
|
# Expose to public internet
|
|
if old_version == 0:
|
|
self.enable()
|
|
elif old_version < 5:
|
|
privileged.fix_incorrect_key_ownership()
|
|
service_privileged.try_restart('rspamd')
|
|
|
|
|
|
class DovecotDropinConfigs(DropinConfigs):
|
|
"""Configure dovecot based on its package version."""
|
|
|
|
def get_target_path(self, path):
|
|
"""Return Path object for a target path."""
|
|
version = '2.3'
|
|
if dovecot.is_version_24():
|
|
version = '2.4'
|
|
|
|
target_path = super().get_target_path(path)
|
|
target_path = target_path.parent / version / target_path.name
|
|
return target_path
|
|
|
|
|
|
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()
|