mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
email: setup: Configure Roundcube
This commit is contained in:
parent
5a9c7e5077
commit
1e712f6bc4
@ -137,6 +137,7 @@ class EmailServerApp(plinth.app.App):
|
||||
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
|
||||
|
||||
|
||||
@ -147,6 +148,7 @@ def setup(helper, old_version=None):
|
||||
helper.call('post', audit.ldap.repair)
|
||||
helper.call('post', audit.spam.repair)
|
||||
helper.call('post', audit.tls.repair)
|
||||
helper.call('post', audit.rcube.repair)
|
||||
for srvname in managed_services:
|
||||
actions.superuser_run('service', ['reload', srvname])
|
||||
# Final step: expose service daemons to public internet
|
||||
|
||||
@ -6,7 +6,8 @@ Provides diagnosis and repair of email server configuration issues
|
||||
from . import domain
|
||||
from . import home
|
||||
from . import ldap
|
||||
from . import rcube
|
||||
from . import spam
|
||||
from . import tls
|
||||
|
||||
__all__ = ['domain', 'home', 'ldap', 'spam', 'tls']
|
||||
__all__ = ['domain', 'home', 'ldap', 'rcube', 'spam', 'tls']
|
||||
|
||||
81
plinth/modules/email_server/audit/rcube.py
Normal file
81
plinth/modules/email_server/audit/rcube.py
Normal file
@ -0,0 +1,81 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from plinth import actions
|
||||
|
||||
from . import models
|
||||
from plinth.modules.email_server.lock import Mutex
|
||||
from plinth.modules.email_server.modconf import ConfigInjector
|
||||
|
||||
|
||||
config_path = '/etc/roundcube/config.inc.php'
|
||||
boundary_pattern = '//[ ]*--[ ]*(BEGIN|END)[ ]+FREEDOMBOX CONFIG$'
|
||||
boundary_format = '//-- {} FREEDOMBOX CONFIG'
|
||||
|
||||
rconf_template = """//
|
||||
// The following section is managed by FreedomBox
|
||||
// Be careful not to edit
|
||||
include_once("/etc/roundcube/freedombox_mail.inc.php");
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
rcube_mutex = Mutex('rcube-config')
|
||||
|
||||
|
||||
def get():
|
||||
translation_table = {
|
||||
'rc_installed': _('RoundCube availability'),
|
||||
'rc_config_header': _('FreedomBox header in RoundCube config'),
|
||||
}
|
||||
output = actions.superuser_run('email_server', ['-i', 'rcube', 'check'])
|
||||
results = json.loads(output)
|
||||
for i in range(0, len(results)):
|
||||
name = translation_table.get(results[i][0], results[i][0])
|
||||
diagnosis = models.Diagnosis(name)
|
||||
if results[i][1] == 'error':
|
||||
diagnosis.error('Failed')
|
||||
results[i] = diagnosis
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def repair():
|
||||
actions.superuser_run('email_server', ['-i', 'rcube', 'set_up'])
|
||||
|
||||
|
||||
def action_check():
|
||||
results = _action_check()
|
||||
print(json.dumps(results))
|
||||
|
||||
|
||||
def _action_check():
|
||||
results = []
|
||||
if not os.path.exists(config_path):
|
||||
results.append(['rc_installed', 'error'])
|
||||
return results
|
||||
|
||||
injector = ConfigInjector(boundary_pattern, boundary_format)
|
||||
if injector.has_header_line(config_path):
|
||||
results.append(['rc_config_header', 'pass'])
|
||||
else:
|
||||
results.append(['rc_config_header', 'error'])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def action_set_up():
|
||||
with rcube_mutex.lock_all():
|
||||
_inject_rcube_config()
|
||||
|
||||
|
||||
def _inject_rcube_config():
|
||||
if not os.path.exists(config_path):
|
||||
logger.warning('Roundcube has not been installed')
|
||||
return
|
||||
logger.info('Opening rcube config file %s', config_path)
|
||||
injector = ConfigInjector(boundary_pattern, boundary_format)
|
||||
injector.do_template_string(rconf_template, config_path)
|
||||
@ -10,6 +10,7 @@ from plinth import actions
|
||||
|
||||
from . import models
|
||||
from plinth.modules.email_server import interproc, lock, postconf
|
||||
from plinth.modules.email_server.modconf import ConfigInjector
|
||||
|
||||
milter_config = {
|
||||
'milter_mail_macros': 'i ' + ' '.join([
|
||||
@ -65,9 +66,8 @@ egress_filter_cleanup_options = {
|
||||
|
||||
# Rspamd config
|
||||
|
||||
rspamd_boundary = re.compile('#[ ]*--[ ]*([A-Z]{3,5})[ ]+FREEDOMBOX CONFIG$')
|
||||
rspamd_header = '#-- BEGIN FREEDOMBOX CONFIG\n'
|
||||
rspamd_footer = '#-- END FREEDOMBOX CONFIG\n'
|
||||
rspamd_re = re.compile('#[ ]*--[ ]*([A-Z]{3,5})[ ]+FREEDOMBOX CONFIG$')
|
||||
rspamd_format = '#-- {} FREEDOMBOX CONFIG'
|
||||
|
||||
rspamd_mutex = lock.Mutex('rspamd-config')
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -103,61 +103,19 @@ def action_set_filter():
|
||||
with postconf.mutex.lock_all():
|
||||
fix_filter(check_filter())
|
||||
|
||||
injector = ConfigInjector(rspamd_re, rspamd_format)
|
||||
with rspamd_mutex.lock_all():
|
||||
# XXX Maybe use globbing?
|
||||
_inject_rspamd_config('override', 'options.inc')
|
||||
_inject_rspamd_config('local', 'milter_headers.conf')
|
||||
_inject_rspamd_config(injector, 'override', 'options.inc')
|
||||
_inject_rspamd_config(injector, 'local', 'milter_headers.conf')
|
||||
|
||||
|
||||
def _inject_rspamd_config(type, name):
|
||||
def _inject_rspamd_config(injector, type, name):
|
||||
template_path = '/etc/plinth/rspamd-config/%s_%s' % (type, name)
|
||||
config_path = '/etc/rspamd/%s.d/%s' % (type, name)
|
||||
|
||||
logger.info('Opening Rspamd config file %s', config_path)
|
||||
|
||||
template = None
|
||||
config = None
|
||||
try:
|
||||
template = open(template_path, 'r')
|
||||
config = open(config_path, 'a+')
|
||||
with interproc.atomically_rewrite(config_path) as scratch:
|
||||
config.seek(0)
|
||||
inject_rspamd_config3(template, config, scratch)
|
||||
finally:
|
||||
if config is not None:
|
||||
config.close()
|
||||
if template is not None:
|
||||
template.close()
|
||||
|
||||
|
||||
def inject_rspamd_config3(template, config, scratch):
|
||||
"""Write modified rspamd config to the `scratch` stream"""
|
||||
# Copy the original up to the config header line
|
||||
for line in config:
|
||||
match = rspamd_boundary.match(line.strip())
|
||||
if match and match.group(1) == 'BEGIN':
|
||||
break
|
||||
scratch.write(line)
|
||||
if not line.endswith('\n'): # in case no new line was at the eof
|
||||
scratch.write('\n')
|
||||
|
||||
# Inject template data
|
||||
scratch.write(rspamd_header)
|
||||
for line in template:
|
||||
scratch.write(line)
|
||||
if not line.endswith('\n'): # in case no new line was at the eof
|
||||
scratch.write('\n')
|
||||
scratch.write(rspamd_footer)
|
||||
|
||||
# Find the config trailer line
|
||||
for line in config:
|
||||
match = rspamd_boundary.match(line.strip())
|
||||
if match and match.group(1) == 'END':
|
||||
break
|
||||
|
||||
# Copy the original
|
||||
for line in config:
|
||||
scratch.write(line) # keep original file ending style
|
||||
injector.do_template_file(template_path, config_path)
|
||||
|
||||
|
||||
def _compile_sieve():
|
||||
|
||||
@ -22,7 +22,9 @@ tls_medium_cipherlist = [
|
||||
postfix_config = {
|
||||
# Enable TLS
|
||||
'smtpd_tls_security_level': 'may',
|
||||
'smtpd_tls_auth_only': 'yes',
|
||||
|
||||
# Allow unencrypted auth on port 25, needed by Roundcube
|
||||
'smtpd_tls_auth_only': 'no',
|
||||
|
||||
# Debugging information
|
||||
'smtpd_tls_received_header': 'yes',
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$config['default_host'] = 'localhost';
|
||||
$config['mail_domain'] = '%n';
|
||||
|
||||
$config['smtp_server'] = 'localhost';
|
||||
$config['smtp_port'] = 25;
|
||||
$config['smtp_helo_host'] = 'localhost';
|
||||
?>
|
||||
78
plinth/modules/email_server/modconf.py
Normal file
78
plinth/modules/email_server/modconf.py
Normal file
@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Generic config modifying utilities"""
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import re
|
||||
|
||||
from . import interproc
|
||||
|
||||
|
||||
class ConfigInjector:
|
||||
def __init__(self, match, generate):
|
||||
self.re_pattern = re.compile(match)
|
||||
self.boundary_fmt = generate + '\n'
|
||||
|
||||
def do_file_des(self, template, config, scratch):
|
||||
"""Write modified config to the `scratch` stream"""
|
||||
if not isinstance(template, io.TextIOBase):
|
||||
raise TypeError('Not a text IO stream: template')
|
||||
self._inject_config3(template, config, scratch)
|
||||
|
||||
def _inject_config3(self, template, config, scratch):
|
||||
# Copy the original config up to header line
|
||||
for line in config:
|
||||
match = self.re_pattern.match(line.strip())
|
||||
if match and match.group(1) == 'BEGIN':
|
||||
break
|
||||
scratch.write(line)
|
||||
if not line.endswith('\n'): # in case no new line was at the eof
|
||||
scratch.write('\n')
|
||||
|
||||
# Write header line
|
||||
scratch.write(self.boundary_fmt.format('BEGIN'))
|
||||
# Write template to scratch
|
||||
for line in template:
|
||||
scratch.write(line)
|
||||
# in case no new line was at the eof
|
||||
if not line.endswith('\n'):
|
||||
scratch.write('\n')
|
||||
# Write footer line
|
||||
scratch.write(self.boundary_fmt.format('END'))
|
||||
|
||||
# Find the trailer line in config
|
||||
for line in config:
|
||||
match = self.re_pattern.match(line.strip())
|
||||
if match and match.group(1) == 'END':
|
||||
break
|
||||
|
||||
# Copy the original
|
||||
for line in config:
|
||||
scratch.write(line) # keep original file ending style
|
||||
|
||||
def do_template_file(self, template_path, config_path):
|
||||
with open(template_path, 'r') as template:
|
||||
with self._open_config(config_path) as (config, scratch):
|
||||
self._inject_config3(template, config, scratch)
|
||||
|
||||
def do_template_string(self, template_string, config_path):
|
||||
with self._open_config(config_path) as (config, scratch):
|
||||
self._inject_config3([template_string], config, scratch)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _open_config(self, config_path):
|
||||
with open(config_path, 'a+') as config:
|
||||
with interproc.atomically_rewrite(config_path) as scratch:
|
||||
config.seek(0)
|
||||
yield config, scratch
|
||||
|
||||
def has_header_line(self, config_path):
|
||||
with open(config_path, 'r') as config_fd:
|
||||
return self._has_header_line(config_fd)
|
||||
|
||||
def _has_header_line(self, config_fd):
|
||||
for line in config_fd:
|
||||
match = self.re_pattern.match(line.strip())
|
||||
if match and match.group(1) == 'BEGIN':
|
||||
return True
|
||||
return False
|
||||
Loading…
x
Reference in New Issue
Block a user