mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +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.ldap.get()])
|
||||||
results.extend([r.summarize() for r in audit.spam.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.tls.get()])
|
||||||
|
results.extend([r.summarize() for r in audit.rcube.get()])
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@ -147,6 +148,7 @@ def setup(helper, old_version=None):
|
|||||||
helper.call('post', audit.ldap.repair)
|
helper.call('post', audit.ldap.repair)
|
||||||
helper.call('post', audit.spam.repair)
|
helper.call('post', audit.spam.repair)
|
||||||
helper.call('post', audit.tls.repair)
|
helper.call('post', audit.tls.repair)
|
||||||
|
helper.call('post', audit.rcube.repair)
|
||||||
for srvname in managed_services:
|
for srvname in managed_services:
|
||||||
actions.superuser_run('service', ['reload', srvname])
|
actions.superuser_run('service', ['reload', srvname])
|
||||||
# Final step: expose service daemons to public internet
|
# 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 domain
|
||||||
from . import home
|
from . import home
|
||||||
from . import ldap
|
from . import ldap
|
||||||
|
from . import rcube
|
||||||
from . import spam
|
from . import spam
|
||||||
from . import tls
|
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 . import models
|
||||||
from plinth.modules.email_server import interproc, lock, postconf
|
from plinth.modules.email_server import interproc, lock, postconf
|
||||||
|
from plinth.modules.email_server.modconf import ConfigInjector
|
||||||
|
|
||||||
milter_config = {
|
milter_config = {
|
||||||
'milter_mail_macros': 'i ' + ' '.join([
|
'milter_mail_macros': 'i ' + ' '.join([
|
||||||
@ -65,9 +66,8 @@ egress_filter_cleanup_options = {
|
|||||||
|
|
||||||
# Rspamd config
|
# Rspamd config
|
||||||
|
|
||||||
rspamd_boundary = re.compile('#[ ]*--[ ]*([A-Z]{3,5})[ ]+FREEDOMBOX CONFIG$')
|
rspamd_re = re.compile('#[ ]*--[ ]*([A-Z]{3,5})[ ]+FREEDOMBOX CONFIG$')
|
||||||
rspamd_header = '#-- BEGIN FREEDOMBOX CONFIG\n'
|
rspamd_format = '#-- {} FREEDOMBOX CONFIG'
|
||||||
rspamd_footer = '#-- END FREEDOMBOX CONFIG\n'
|
|
||||||
|
|
||||||
rspamd_mutex = lock.Mutex('rspamd-config')
|
rspamd_mutex = lock.Mutex('rspamd-config')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -103,61 +103,19 @@ def action_set_filter():
|
|||||||
with postconf.mutex.lock_all():
|
with postconf.mutex.lock_all():
|
||||||
fix_filter(check_filter())
|
fix_filter(check_filter())
|
||||||
|
|
||||||
|
injector = ConfigInjector(rspamd_re, rspamd_format)
|
||||||
with rspamd_mutex.lock_all():
|
with rspamd_mutex.lock_all():
|
||||||
# XXX Maybe use globbing?
|
# XXX Maybe use globbing?
|
||||||
_inject_rspamd_config('override', 'options.inc')
|
_inject_rspamd_config(injector, 'override', 'options.inc')
|
||||||
_inject_rspamd_config('local', 'milter_headers.conf')
|
_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)
|
template_path = '/etc/plinth/rspamd-config/%s_%s' % (type, name)
|
||||||
config_path = '/etc/rspamd/%s.d/%s' % (type, name)
|
config_path = '/etc/rspamd/%s.d/%s' % (type, name)
|
||||||
|
|
||||||
logger.info('Opening Rspamd config file %s', config_path)
|
logger.info('Opening Rspamd config file %s', config_path)
|
||||||
|
injector.do_template_file(template_path, 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
|
|
||||||
|
|
||||||
|
|
||||||
def _compile_sieve():
|
def _compile_sieve():
|
||||||
|
|||||||
@ -22,7 +22,9 @@ tls_medium_cipherlist = [
|
|||||||
postfix_config = {
|
postfix_config = {
|
||||||
# Enable TLS
|
# Enable TLS
|
||||||
'smtpd_tls_security_level': 'may',
|
'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
|
# Debugging information
|
||||||
'smtpd_tls_received_header': 'yes',
|
'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