email: setup: Configure Roundcube

This commit is contained in:
fliu 2021-08-17 09:10:45 +00:00 committed by Sunil Mohan Adapa
parent 5a9c7e5077
commit 1e712f6bc4
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
7 changed files with 182 additions and 52 deletions

View File

@ -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

View File

@ -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']

View 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)

View File

@ -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():

View File

@ -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',

View File

@ -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';
?>

View 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