mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-11-03 19:30:54 +00:00 
			
		
		
		
	Merge branch 'main' of https://github.com/mail-in-a-box/mailinabox
The roundcube password plugin is not disabled. # Conflicts: # management/utils.py # setup/start.sh # setup/system.sh # setup/webmail.sh # tools/editconf.py
This commit is contained in:
		
						commit
						190d7195d3
					
				
							
								
								
									
										21
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@ -1,6 +1,27 @@
 | 
			
		||||
CHANGELOG
 | 
			
		||||
=========
 | 
			
		||||
 | 
			
		||||
In Development
 | 
			
		||||
--------------
 | 
			
		||||
 | 
			
		||||
System:
 | 
			
		||||
 | 
			
		||||
* fail2ban didn't start after setup.
 | 
			
		||||
 | 
			
		||||
Mail:
 | 
			
		||||
 | 
			
		||||
* Disable Roundcube password plugin since it was corrupting the user database.
 | 
			
		||||
 | 
			
		||||
Control panel:
 | 
			
		||||
 | 
			
		||||
* Fix changing existing backup settings when the rsync type is used.
 | 
			
		||||
* Allow setting a custom port for rsync backups.
 | 
			
		||||
* Fixes to DNS lookups during status checks when there are timeouts, enforce timeouts better.
 | 
			
		||||
* A new check is added to ensure fail2ban is running.
 | 
			
		||||
* Fixed a color.
 | 
			
		||||
* Disable Roundcube password plugin since it was corrupting the user database
 | 
			
		||||
* Improve error messages in the management tools when external command-line tools are run.
 | 
			
		||||
 | 
			
		||||
Version 60.1 (October 30, 2022)
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
--- a/Net/LDAP2/Entry.php
 | 
			
		||||
+++ b/Net/LDAP2/Entry.php
 | 
			
		||||
@@ -363,10 +363,9 @@ protected function setAttributes($attributes = null)
 | 
			
		||||
             $attributes = array();
 | 
			
		||||
             do {
 | 
			
		||||
                 if (empty($attr)) {
 | 
			
		||||
-                    $ber  = null;
 | 
			
		||||
-                    $attr = @ldap_first_attribute($this->_link, $this->_entry, $ber);
 | 
			
		||||
+                    $attr = @ldap_first_attribute($this->_link, $this->_entry);
 | 
			
		||||
                 } else {
 | 
			
		||||
-                    $attr = @ldap_next_attribute($this->_link, $this->_entry, $ber);
 | 
			
		||||
+                    $attr = @ldap_next_attribute($this->_link, $this->_entry);
 | 
			
		||||
                 }
 | 
			
		||||
                 if ($attr) {
 | 
			
		||||
                     $func = 'ldap_get_values'; // standard function to fetch value
 | 
			
		||||
@ -223,9 +223,18 @@ def get_duplicity_additional_args(env):
 | 
			
		||||
	config = get_backup_config(env)
 | 
			
		||||
 | 
			
		||||
	if get_target_type(config) == 'rsync':
 | 
			
		||||
		# Extract a port number for the ssh transport.  Duplicity accepts the
 | 
			
		||||
		# optional port number syntax in the target, but it doesn't appear to act
 | 
			
		||||
		# on it, so we set the ssh port explicitly via the duplicity options.
 | 
			
		||||
		from urllib.parse import urlsplit
 | 
			
		||||
		try:
 | 
			
		||||
			port = urlsplit(config["target"]).port
 | 
			
		||||
		except ValueError:
 | 
			
		||||
			port = 22
 | 
			
		||||
 | 
			
		||||
		return [
 | 
			
		||||
			"--ssh-options= -i /root/.ssh/id_rsa_miab",
 | 
			
		||||
			"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
 | 
			
		||||
			f"--ssh-options= -i /root/.ssh/id_rsa_miab -p {port}",
 | 
			
		||||
			f"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p {port} -i /root/.ssh/id_rsa_miab\"",
 | 
			
		||||
		]
 | 
			
		||||
	elif get_target_type(config) == 's3':
 | 
			
		||||
		# See note about hostname in get_duplicity_target_url.
 | 
			
		||||
@ -438,6 +447,14 @@ def list_target_files(config):
 | 
			
		||||
		rsync_fn_size_re = re.compile(r'.*    ([^ ]*) [^ ]* [^ ]* (.*)')
 | 
			
		||||
		rsync_target = '{host}:{path}'
 | 
			
		||||
 | 
			
		||||
		# Strip off any trailing port specifier because it's not valid in rsync's
 | 
			
		||||
		# DEST syntax.  Explicitly set the port number for the ssh transport.
 | 
			
		||||
		user_host, *_ = target.netloc.rsplit(':', 1)
 | 
			
		||||
		try:
 | 
			
		||||
			port = target.port
 | 
			
		||||
		except ValueError:
 | 
			
		||||
			 port = 22
 | 
			
		||||
 | 
			
		||||
		target_path = target.path
 | 
			
		||||
		if not target_path.endswith('/'):
 | 
			
		||||
			target_path = target_path + '/'
 | 
			
		||||
@ -446,11 +463,11 @@ def list_target_files(config):
 | 
			
		||||
 | 
			
		||||
		rsync_command = [ 'rsync',
 | 
			
		||||
					'-e',
 | 
			
		||||
					'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes',
 | 
			
		||||
					f'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes -p {port}',
 | 
			
		||||
					'--list-only',
 | 
			
		||||
					'-r',
 | 
			
		||||
					rsync_target.format(
 | 
			
		||||
						host=target.netloc,
 | 
			
		||||
						host=user_host,
 | 
			
		||||
						path=target_path)
 | 
			
		||||
				]
 | 
			
		||||
 | 
			
		||||
@ -561,7 +578,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
 | 
			
		||||
 | 
			
		||||
	# Merge in anything written to custom.yaml.
 | 
			
		||||
	try:
 | 
			
		||||
		custom_config = rtyaml.load(open(os.path.join(backup_root, 'custom.yaml')))
 | 
			
		||||
		with open(os.path.join(backup_root, 'custom.yaml'), 'r') as f:
 | 
			
		||||
			custom_config = rtyaml.load(f)
 | 
			
		||||
		if not isinstance(custom_config, dict): raise ValueError() # caught below
 | 
			
		||||
		config.update(custom_config)
 | 
			
		||||
	except:
 | 
			
		||||
@ -586,7 +604,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
 | 
			
		||||
		config["target"] = "file://" + config["file_target_directory"]
 | 
			
		||||
	ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub')
 | 
			
		||||
	if os.path.exists(ssh_pub_key):
 | 
			
		||||
		config["ssh_pub_key"] = open(ssh_pub_key, 'r').read()
 | 
			
		||||
		with open(ssh_pub_key, 'r') as f:
 | 
			
		||||
			config["ssh_pub_key"] = f.read()
 | 
			
		||||
 | 
			
		||||
	return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,8 @@ def read_password():
 | 
			
		||||
    return first
 | 
			
		||||
 | 
			
		||||
def setup_key_auth(mgmt_uri):
 | 
			
		||||
	key = open('/var/lib/mailinabox/api.key').read().strip()
 | 
			
		||||
	with open('/var/lib/mailinabox/api.key', 'r') as f:
 | 
			
		||||
		key = f.read().strip()
 | 
			
		||||
 | 
			
		||||
	auth_handler = urllib.request.HTTPBasicAuthHandler()
 | 
			
		||||
	auth_handler.add_password(
 | 
			
		||||
 | 
			
		||||
@ -831,7 +831,8 @@ def write_opendkim_tables(domains, env):
 | 
			
		||||
 | 
			
		||||
def get_custom_dns_config(env, only_real_records=False):
 | 
			
		||||
	try:
 | 
			
		||||
		custom_dns = rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')))
 | 
			
		||||
		with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), 'r') as f:
 | 
			
		||||
			custom_dns = rtyaml.load(f)
 | 
			
		||||
		if not isinstance(custom_dns, dict): raise ValueError() # caught below
 | 
			
		||||
	except:
 | 
			
		||||
		return [ ]
 | 
			
		||||
@ -1008,6 +1009,7 @@ def set_custom_dns_record(qname, rtype, value, action, env):
 | 
			
		||||
def get_secondary_dns(custom_dns, mode=None):
 | 
			
		||||
	resolver = dns.resolver.get_default_resolver()
 | 
			
		||||
	resolver.timeout = 10
 | 
			
		||||
	resolver.lifetime = 10
 | 
			
		||||
 | 
			
		||||
	values = []
 | 
			
		||||
	for qname, rtype, value in custom_dns:
 | 
			
		||||
@ -1025,10 +1027,17 @@ def get_secondary_dns(custom_dns, mode=None):
 | 
			
		||||
			# doesn't.
 | 
			
		||||
			if not hostname.startswith("xfr:"):
 | 
			
		||||
				if mode == "xfr":
 | 
			
		||||
					response = dns.resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
 | 
			
		||||
					values.extend(map(str, response))
 | 
			
		||||
					response = dns.resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
 | 
			
		||||
					values.extend(map(str, response))
 | 
			
		||||
					try:
 | 
			
		||||
						response = resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
 | 
			
		||||
						values.extend(map(str, response))
 | 
			
		||||
					except dns.exception.DNSException:
 | 
			
		||||
						pass
 | 
			
		||||
						
 | 
			
		||||
					try:
 | 
			
		||||
						response = resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
 | 
			
		||||
						values.extend(map(str, response))
 | 
			
		||||
					except dns.exception.DNSException:
 | 
			
		||||
						pass
 | 
			
		||||
					continue
 | 
			
		||||
				values.append(hostname)
 | 
			
		||||
 | 
			
		||||
@ -1046,15 +1055,17 @@ def set_secondary_dns(hostnames, env):
 | 
			
		||||
		# Validate that all hostnames are valid and that all zone-xfer IP addresses are valid.
 | 
			
		||||
		resolver = dns.resolver.get_default_resolver()
 | 
			
		||||
		resolver.timeout = 5
 | 
			
		||||
		resolver.lifetime = 5
 | 
			
		||||
		
 | 
			
		||||
		for item in hostnames:
 | 
			
		||||
			if not item.startswith("xfr:"):
 | 
			
		||||
				# Resolve hostname.
 | 
			
		||||
				try:
 | 
			
		||||
					response = resolver.resolve(item, "A")
 | 
			
		||||
				except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
 | 
			
		||||
				except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
 | 
			
		||||
					try:
 | 
			
		||||
						response = resolver.resolve(item, "AAAA")
 | 
			
		||||
					except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
 | 
			
		||||
					except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
 | 
			
		||||
						raise ValueError("Could not resolve the IP address of %s." % item)
 | 
			
		||||
			else:
 | 
			
		||||
				# Validate IP address.
 | 
			
		||||
@ -1087,7 +1098,7 @@ def get_custom_dns_records(custom_dns, qname, rtype):
 | 
			
		||||
def build_recommended_dns(env):
 | 
			
		||||
	ret = []
 | 
			
		||||
	for (domain, zonefile, records) in build_zones(env):
 | 
			
		||||
		# remove records that we don't dislay
 | 
			
		||||
		# remove records that we don't display
 | 
			
		||||
		records = [r for r in records if r[3] is not False]
 | 
			
		||||
 | 
			
		||||
		# put Required at the top, then Recommended, then everythiing else
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,8 @@ def scan_files(collector):
 | 
			
		||||
            continue
 | 
			
		||||
        elif fn[-3:] == '.gz':
 | 
			
		||||
            tmp_file = tempfile.NamedTemporaryFile()
 | 
			
		||||
            shutil.copyfileobj(gzip.open(fn), tmp_file)
 | 
			
		||||
            with gzip.open(fn, 'rb') as f:
 | 
			
		||||
                shutil.copyfileobj(f, tmp_file)
 | 
			
		||||
 | 
			
		||||
        if VERBOSE:
 | 
			
		||||
            print("Processing file", fn, "...")
 | 
			
		||||
 | 
			
		||||
@ -548,7 +548,8 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
 | 
			
		||||
	# Second, check that the certificate matches the private key.
 | 
			
		||||
	if ssl_private_key is not None:
 | 
			
		||||
		try:
 | 
			
		||||
			priv_key = load_pem(open(ssl_private_key, 'rb').read())
 | 
			
		||||
			with open(ssl_private_key, 'rb') as f:
 | 
			
		||||
				priv_key = load_pem(f.read())
 | 
			
		||||
		except ValueError as e:
 | 
			
		||||
			return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -107,6 +107,12 @@ def run_services_checks(env, output, pool):
 | 
			
		||||
		fatal = fatal or fatal2
 | 
			
		||||
		output2.playback(output)
 | 
			
		||||
 | 
			
		||||
	# Check fail2ban.
 | 
			
		||||
	code, ret = shell('check_output', ["fail2ban-client", "status"], capture_stderr=True, trap=True)
 | 
			
		||||
	if code != 0:
 | 
			
		||||
		output.print_error("fail2ban is not running.")
 | 
			
		||||
		all_running = False
 | 
			
		||||
 | 
			
		||||
	if all_running:
 | 
			
		||||
		output.print_ok("All system services are running.")
 | 
			
		||||
 | 
			
		||||
@ -219,7 +225,8 @@ def check_ssh_password(env, output):
 | 
			
		||||
	# the configuration file.
 | 
			
		||||
	if not os.path.exists("/etc/ssh/sshd_config"):
 | 
			
		||||
		return
 | 
			
		||||
	sshd = open("/etc/ssh/sshd_config").read()
 | 
			
		||||
	with open("/etc/ssh/sshd_config", "r") as f:
 | 
			
		||||
		sshd = f.read()
 | 
			
		||||
	if re.search("\nPasswordAuthentication\s+yes", sshd) \
 | 
			
		||||
		or not re.search("\nPasswordAuthentication\s+no", sshd):
 | 
			
		||||
		output.print_error("""The SSH server on this machine permits password-based login. A more secure
 | 
			
		||||
@ -320,6 +327,8 @@ def run_network_checks(env, output):
 | 
			
		||||
		output.print_ok("IP address is not blacklisted by zen.spamhaus.org.")
 | 
			
		||||
	elif zen == "[timeout]":
 | 
			
		||||
		output.print_warning("Connection to zen.spamhaus.org timed out. We could not determine whether your server's IP address is blacklisted. Please try again later.")
 | 
			
		||||
	elif zen == "[Not Set]":
 | 
			
		||||
		output.print_warning("Could not connect to zen.spamhaus.org. We could not determine whether your server's IP address is blacklisted. Please try again later.")
 | 
			
		||||
	else:
 | 
			
		||||
		output.print_error("""The IP address of this machine %s is listed in the Spamhaus Block List (code %s),
 | 
			
		||||
			which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
 | 
			
		||||
@ -553,7 +562,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
 | 
			
		||||
		for ns in custom_secondary_ns:
 | 
			
		||||
			# We must first resolve the nameserver to an IP address so we can query it.
 | 
			
		||||
			ns_ips = query_dns(ns, "A")
 | 
			
		||||
			if not ns_ips:
 | 
			
		||||
			if not ns_ips or ns_ips in {'[Not Set]', '[timeout]'}:
 | 
			
		||||
				output.print_error("Secondary nameserver %s is not valid (it doesn't resolve to an IP address)." % ns)
 | 
			
		||||
				continue
 | 
			
		||||
			# Choose the first IP if nameserver returns multiple
 | 
			
		||||
@ -604,7 +613,8 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
 | 
			
		||||
			# record that we suggest using is for the KSK (and that's how the DS records were generated).
 | 
			
		||||
			# We'll also give the nice name for the key algorithm.
 | 
			
		||||
			dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg]))
 | 
			
		||||
			dnsssec_pubkey = open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')).read().split("\t")[3].split(" ")[3]
 | 
			
		||||
			with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key'), 'r') as f:
 | 
			
		||||
				dnsssec_pubkey = f.read().split("\t")[3].split(" ")[3]
 | 
			
		||||
 | 
			
		||||
			expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = {
 | 
			
		||||
				"record": rr_ds,
 | 
			
		||||
@ -756,6 +766,8 @@ def check_mail_domain(domain, env, output):
 | 
			
		||||
		output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.")
 | 
			
		||||
	elif dbl == "[timeout]":
 | 
			
		||||
		output.print_warning("Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
 | 
			
		||||
	elif dbl == "[Not Set]":
 | 
			
		||||
		output.print_warning("Could not connect to dbl.spamhaus.org. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
 | 
			
		||||
	else:
 | 
			
		||||
		output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s),
 | 
			
		||||
			which may prevent recipients from receiving your mail.
 | 
			
		||||
@ -800,12 +812,17 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None, as_list=False):
 | 
			
		||||
	# running bind server), or if the 'at' argument is specified, use that host
 | 
			
		||||
	# as the nameserver.
 | 
			
		||||
	resolver = dns.resolver.get_default_resolver()
 | 
			
		||||
	if at:
 | 
			
		||||
	
 | 
			
		||||
	# Make sure at is not a string that cannot be used as a nameserver
 | 
			
		||||
	if at and at not in {'[Not set]', '[timeout]'}:
 | 
			
		||||
		resolver = dns.resolver.Resolver()
 | 
			
		||||
		resolver.nameservers = [at]
 | 
			
		||||
 | 
			
		||||
	# Set a timeout so that a non-responsive server doesn't hold us back.
 | 
			
		||||
	resolver.timeout = 5
 | 
			
		||||
	# The number of seconds to spend trying to get an answer to the question. If the
 | 
			
		||||
	# lifetime expires a dns.exception.Timeout exception will be raised.
 | 
			
		||||
	resolver.lifetime = 5
 | 
			
		||||
 | 
			
		||||
	# Do the query.
 | 
			
		||||
	try:
 | 
			
		||||
@ -959,7 +976,8 @@ def run_and_output_changes(env, pool):
 | 
			
		||||
	# Load previously saved status checks.
 | 
			
		||||
	cache_fn = "/var/cache/mailinabox/status_checks.json"
 | 
			
		||||
	if os.path.exists(cache_fn):
 | 
			
		||||
		prev = json.load(open(cache_fn))
 | 
			
		||||
		with open(cache_fn, 'r') as f:
 | 
			
		||||
			prev = json.load(f)
 | 
			
		||||
 | 
			
		||||
		# execute hooks
 | 
			
		||||
		hook_data = {
 | 
			
		||||
 | 
			
		||||
@ -72,11 +72,6 @@
 | 
			
		||||
        html {
 | 
			
		||||
          filter: invert(100%) hue-rotate(180deg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* Set explicit background color (necessary for Firefox) */
 | 
			
		||||
        html {
 | 
			
		||||
          background-color: #111;
 | 
			
		||||
        }
 | 
			
		||||
          
 | 
			
		||||
        /* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */
 | 
			
		||||
        .form-control {
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,10 @@
 | 
			
		||||
    <label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label>
 | 
			
		||||
    <div class="col-sm-8">
 | 
			
		||||
      <input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host">
 | 
			
		||||
      <div class="small" style="margin-top: 2px">
 | 
			
		||||
	The hostname at your rsync provider, e.g. <tt>da2327.rsync.net</tt>.  Optionally includes a colon
 | 
			
		||||
	and the provider's non-standard ssh port number, e.g. <tt>u215843.your-storagebox.de:23</tt>. 
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="form-group backup-target-rsync">
 | 
			
		||||
@ -259,12 +263,11 @@ function show_custom_backup() {
 | 
			
		||||
        } else if (r.target == "off") {
 | 
			
		||||
          $("#backup-target-type").val("off");
 | 
			
		||||
        } else if (r.target.substring(0, 8) == "rsync://") {
 | 
			
		||||
          $("#backup-target-type").val("rsync");
 | 
			
		||||
          var path = r.target.substring(8).split('//');
 | 
			
		||||
          var host_parts = path.shift().split('@');
 | 
			
		||||
          $("#backup-target-rsync-user").val(host_parts[0]);
 | 
			
		||||
          $("#backup-target-rsync-host").val(host_parts[1]);
 | 
			
		||||
          $("#backup-target-rsync-path").val('/'+path[0]);
 | 
			
		||||
          const spec = url_split(r.target);
 | 
			
		||||
          $("#backup-target-type").val(spec.scheme);
 | 
			
		||||
	  $("#backup-target-rsync-user").val(spec.user);
 | 
			
		||||
	  $("#backup-target-rsync-host").val(spec.host);
 | 
			
		||||
	  $("#backup-target-rsync-path").val(spec.path);
 | 
			
		||||
        } else if (r.target.substring(0, 5) == "s3://") {
 | 
			
		||||
          $("#backup-target-type").val("s3");
 | 
			
		||||
          var hostpath = r.target.substring(5).split('/');
 | 
			
		||||
@ -344,4 +347,31 @@ function init_inputs(target_type) {
 | 
			
		||||
    set_host($('#backup-target-s3-host-select').val());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Return a two-element array of the substring preceding and the substring following
 | 
			
		||||
// the first occurence of separator in string. Return [undefined, string] if the
 | 
			
		||||
// separator does not appear in string.
 | 
			
		||||
const split1_rest = (string, separator) => {
 | 
			
		||||
    const index = string.indexOf(separator);
 | 
			
		||||
    return (index >= 0) ? [string.substring(0, index), string.substring(index + separator.length)] : [undefined, string];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Note: The manifest JS URL class does not work in some security-conscious
 | 
			
		||||
// settings, e.g. Brave browser, so we roll our own that handles only what we need.
 | 
			
		||||
//
 | 
			
		||||
// Use greedy separator parsing to get parts of a MIAB backup target url.
 | 
			
		||||
// Note: path will not include a leading forward slash '/'
 | 
			
		||||
const url_split = url => {
 | 
			
		||||
    const [ scheme, scheme_rest ] = split1_rest(url, '://');
 | 
			
		||||
    const [ user, user_rest ] = split1_rest(scheme_rest, '@');
 | 
			
		||||
    const [ host, path ] = split1_rest(user_rest, '/');
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
	scheme,
 | 
			
		||||
	user,
 | 
			
		||||
	host,
 | 
			
		||||
	path,
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
  
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -36,8 +36,9 @@ def load_environment():
 | 
			
		||||
def load_env_vars_from_file(fn, strip_quotes=False, merge_env=None):
 | 
			
		||||
    # Load settings from a KEY=VALUE file.
 | 
			
		||||
    env = Environment()
 | 
			
		||||
    for line in open(fn):
 | 
			
		||||
        env.setdefault(*line.strip().split("=", 1))
 | 
			
		||||
    with open(fn, 'r') as f:
 | 
			
		||||
        for line in f:
 | 
			
		||||
            env.setdefault(*line.strip().split("=", 1))
 | 
			
		||||
    if strip_quotes:
 | 
			
		||||
        for k in env: env[k]=('' if env[k] is None else env[k].strip('"'))
 | 
			
		||||
    if merge_env is not None:
 | 
			
		||||
@ -61,7 +62,8 @@ def load_settings(env):
 | 
			
		||||
    import rtyaml
 | 
			
		||||
    fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
 | 
			
		||||
    try:
 | 
			
		||||
        config = rtyaml.load(open(fn, "r"))
 | 
			
		||||
        with open(fn, "r") as f:
 | 
			
		||||
            config = rtyaml.load(f)
 | 
			
		||||
        if not isinstance(config, dict): raise ValueError() # caught below
 | 
			
		||||
        return config
 | 
			
		||||
    except:
 | 
			
		||||
@ -146,13 +148,16 @@ def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, tr
 | 
			
		||||
    if method == "check_output" and input is not None:
 | 
			
		||||
        kwargs['input'] = input
 | 
			
		||||
 | 
			
		||||
    if not trap:
 | 
			
		||||
    try:
 | 
			
		||||
        ret = getattr(subprocess, method)(cmd_args, **kwargs)
 | 
			
		||||
    else:
 | 
			
		||||
        try:
 | 
			
		||||
            ret = getattr(subprocess, method)(cmd_args, **kwargs)
 | 
			
		||||
            code = 0
 | 
			
		||||
        except subprocess.CalledProcessError as e:
 | 
			
		||||
        code = 0
 | 
			
		||||
    except subprocess.CalledProcessError as e:
 | 
			
		||||
        if not trap:
 | 
			
		||||
            # Reformat exception.
 | 
			
		||||
            msg = "Command failed with exit code {}: {}".format(e.returncode, subprocess.list2cmdline(cmd_args))
 | 
			
		||||
            if e.output: msg += "\n\nOutput:\n" + e.output
 | 
			
		||||
            raise Exception(msg)
 | 
			
		||||
        else:
 | 
			
		||||
            ret = e.output
 | 
			
		||||
            code = e.returncode
 | 
			
		||||
    if not return_bytes and isinstance(ret, bytes): ret = ret.decode("utf8")
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,8 @@ def get_web_domains_with_root_overrides(env):
 | 
			
		||||
	root_overrides = { }
 | 
			
		||||
	nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
 | 
			
		||||
	if os.path.exists(nginx_conf_custom_fn):
 | 
			
		||||
		custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
 | 
			
		||||
		with open(nginx_conf_custom_fn, 'r') as f:
 | 
			
		||||
			custom_settings = rtyaml.load(f)
 | 
			
		||||
		for domain, settings in custom_settings.items():
 | 
			
		||||
			for type, value in [('redirect', settings.get('redirects', {}).get('/')),
 | 
			
		||||
				('proxy', settings.get('proxies', {}).get('/'))]:
 | 
			
		||||
@ -86,13 +87,18 @@ def do_web_update(env):
 | 
			
		||||
	# Pre-load what SSL certificates we will use for each domain.
 | 
			
		||||
	ssl_certificates = get_ssl_certificates(env)
 | 
			
		||||
 | 
			
		||||
	# Helper for reading config files and templates
 | 
			
		||||
	def read_conf(conf_fn):
 | 
			
		||||
		with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn), "r") as f:
 | 
			
		||||
			return f.read()
 | 
			
		||||
 | 
			
		||||
	# Build an nginx configuration file.
 | 
			
		||||
	nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
 | 
			
		||||
	nginx_conf = read_conf("nginx-top.conf")
 | 
			
		||||
 | 
			
		||||
	# Load the templates.
 | 
			
		||||
	template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
 | 
			
		||||
	template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read()
 | 
			
		||||
	template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
 | 
			
		||||
	template0 = read_conf("nginx.conf")
 | 
			
		||||
	template1 = read_conf("nginx-alldomains.conf")
 | 
			
		||||
	template2 = read_conf("nginx-primaryonly.conf")
 | 
			
		||||
	template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
 | 
			
		||||
 | 
			
		||||
	# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
 | 
			
		||||
@ -160,11 +166,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
 | 
			
		||||
	def hashfile(filepath):
 | 
			
		||||
		import hashlib
 | 
			
		||||
		sha1 = hashlib.sha1()
 | 
			
		||||
		f = open(filepath, 'rb')
 | 
			
		||||
		try:
 | 
			
		||||
		with open(filepath, 'rb') as f:
 | 
			
		||||
			sha1.update(f.read())
 | 
			
		||||
		finally:
 | 
			
		||||
			f.close()
 | 
			
		||||
		return sha1.hexdigest()
 | 
			
		||||
	nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
 | 
			
		||||
 | 
			
		||||
@ -172,7 +175,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
 | 
			
		||||
	hsts = "yes"
 | 
			
		||||
	nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
 | 
			
		||||
	if os.path.exists(nginx_conf_custom_fn):
 | 
			
		||||
		yaml = rtyaml.load(open(nginx_conf_custom_fn))
 | 
			
		||||
		with open(nginx_conf_custom_fn, 'r') as f:
 | 
			
		||||
			yaml = rtyaml.load(f)
 | 
			
		||||
		if domain in yaml:
 | 
			
		||||
			yaml = yaml[domain]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
Mail-in-a-Box Security Guide
 | 
			
		||||
============================
 | 
			
		||||
 | 
			
		||||
Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
 | 
			
		||||
Mail-in-a-Box turns a fresh Ubuntu 22.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
 | 
			
		||||
 | 
			
		||||
This page documents the security posture of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -211,13 +211,13 @@ chmod -R o-rwx /etc/dovecot
 | 
			
		||||
 | 
			
		||||
# Ensure mailbox files have a directory that exists and are owned by the mail user.
 | 
			
		||||
mkdir -p $STORAGE_ROOT/mail/mailboxes
 | 
			
		||||
chown -R mail.mail $STORAGE_ROOT/mail/mailboxes
 | 
			
		||||
chown -R mail:mail $STORAGE_ROOT/mail/mailboxes
 | 
			
		||||
 | 
			
		||||
# Same for the sieve scripts.
 | 
			
		||||
mkdir -p $STORAGE_ROOT/mail/sieve
 | 
			
		||||
mkdir -p $STORAGE_ROOT/mail/sieve/global_before
 | 
			
		||||
mkdir -p $STORAGE_ROOT/mail/sieve/global_after
 | 
			
		||||
chown -R mail.mail $STORAGE_ROOT/mail/sieve
 | 
			
		||||
chown -R mail:mail $STORAGE_ROOT/mail/sieve
 | 
			
		||||
 | 
			
		||||
# Allow the IMAP/POP ports in the firewall.
 | 
			
		||||
ufw_allow imaps
 | 
			
		||||
 | 
			
		||||
@ -44,8 +44,8 @@ contact.admin.always_send warning critical
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
# The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi
 | 
			
		||||
chown munin. /var/log/munin/munin-cgi-html.log
 | 
			
		||||
chown munin. /var/log/munin/munin-cgi-graph.log
 | 
			
		||||
chown munin /var/log/munin/munin-cgi-html.log
 | 
			
		||||
chown munin /var/log/munin/munin-cgi-graph.log
 | 
			
		||||
 | 
			
		||||
# ensure munin-node knows the name of this machine
 | 
			
		||||
# and reduce logging level to warning
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@ InstallNextcloud() {
 | 
			
		||||
	# Make sure permissions are correct or the upgrade step won't run.
 | 
			
		||||
	# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
 | 
			
		||||
	# that error.
 | 
			
		||||
	chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud || /bin/true
 | 
			
		||||
	chown -f -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud || /bin/true
 | 
			
		||||
 | 
			
		||||
	# If this isn't a new installation, immediately run the upgrade script.
 | 
			
		||||
	# Then check for success (0=ok and 3=no upgrade needed, both are success).
 | 
			
		||||
@ -289,7 +289,7 @@ EOF
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
	# Set permissions
 | 
			
		||||
	chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
 | 
			
		||||
	chown -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
 | 
			
		||||
 | 
			
		||||
	# Execute Nextcloud's setup step, which creates the Nextcloud sqlite database.
 | 
			
		||||
	# It also wipes it if it exists. And it updates config.php with database
 | 
			
		||||
@ -341,7 +341,7 @@ var_export(\$CONFIG);
 | 
			
		||||
echo ";";
 | 
			
		||||
?>
 | 
			
		||||
EOF
 | 
			
		||||
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
 | 
			
		||||
chown www-data:www-data $STORAGE_ROOT/owncloud/config.php
 | 
			
		||||
 | 
			
		||||
# Enable/disable apps. Note that this must be done after the Nextcloud setup.
 | 
			
		||||
# The firstrunwizard gave Josh all sorts of problems, so disabling that.
 | 
			
		||||
 | 
			
		||||
@ -101,8 +101,8 @@ fi
 | 
			
		||||
f=$STORAGE_ROOT
 | 
			
		||||
while [[ $f != / ]]; do chmod a+rx "$f"; f=$(dirname "$f"); done;
 | 
			
		||||
if [ ! -f $STORAGE_ROOT/mailinabox-ldap.version ]; then
 | 
			
		||||
	echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox-ldap.version
 | 
			
		||||
	chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox-ldap.version
 | 
			
		||||
	setup/migrate.py --current > $STORAGE_ROOT/mailinabox-ldap.version
 | 
			
		||||
	chown $STORAGE_USER:$STORAGE_USER $STORAGE_ROOT/mailinabox-ldap.version
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# normalize the directory path for setup mods
 | 
			
		||||
 | 
			
		||||
@ -439,3 +439,4 @@ tools/editconf.py \
 | 
			
		||||
systemctl daemon-reload
 | 
			
		||||
systemctl enable -q ehdd-unattended-upgrades-after.path
 | 
			
		||||
systemctl start -q ehdd-unattended-upgrades-after.path
 | 
			
		||||
systemctl enable fail2ban
 | 
			
		||||
 | 
			
		||||
@ -109,6 +109,31 @@ if [ $needs_update == 1 ]; then
 | 
			
		||||
	echo $UPDATE_KEY > ${RCM_DIR}/version
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# ### TEMPORARY PATCHES
 | 
			
		||||
 | 
			
		||||
# vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
 | 
			
		||||
# REMOVE BELOW ONCE ROUNDCUBE INCLUDES A PEAR/NET_LDAP2 > 2.2.0
 | 
			
		||||
#
 | 
			
		||||
# Core php (>=8.0) changed the ldap ABI, which breaks the password
 | 
			
		||||
# plugin (which uses pear/net_ldap2 that itself calls the PHP ldap
 | 
			
		||||
# api). There is an unreleased, but accepted, fix that we apply here
 | 
			
		||||
# manually. see:
 | 
			
		||||
# https://github.com/pear/Net_LDAP2/commit/1cacdebcf6fe82718e5fa701c1ff688405e0f5d9
 | 
			
		||||
#
 | 
			
		||||
# The patch below is from github for the commit, which will presumably
 | 
			
		||||
# be included with the next net_ldap2 release.
 | 
			
		||||
#
 | 
			
		||||
# All this can be removed once the net_ldap2 library is released with
 | 
			
		||||
# the fix *AND* roundcube incorporates it with it's release (MIAB is
 | 
			
		||||
# not using composer).
 | 
			
		||||
if grep ldap_first_attribute "/usr/local/lib/roundcubemail/vendor/pear/net_ldap2/Net/LDAP2/Entry.php" | grep -F '$ber' >/dev/null; then
 | 
			
		||||
    patch -p1 --unified --quiet --directory=/usr/local/lib/roundcubemail/vendor/pear/net_ldap2 <$(pwd)/conf/roundcubemail/pear_net_ldap2.1cacdebcf6fe82718e5fa701c1ff688405e0f5d9.diff
 | 
			
		||||
elif [ $needs_update = 1 ]; then
 | 
			
		||||
    say_verbose "Reminder: it is safe to remove net_ldap2 patch applied by webmail.sh"
 | 
			
		||||
fi
 | 
			
		||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ### Configuring Roundcube
 | 
			
		||||
 | 
			
		||||
# Generate a secret key of PHP-string-safe characters appropriate
 | 
			
		||||
@ -223,7 +248,7 @@ EOF
 | 
			
		||||
 | 
			
		||||
# Create writable directories.
 | 
			
		||||
mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
 | 
			
		||||
chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
 | 
			
		||||
chown -R www-data:www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
 | 
			
		||||
 | 
			
		||||
# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
 | 
			
		||||
sudo -u www-data touch /var/log/roundcubemail/errors.log
 | 
			
		||||
@ -252,8 +277,8 @@ tools/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php \
 | 
			
		||||
	"\$config['password_minimum_length']=8;"
 | 
			
		||||
 | 
			
		||||
# Fix Carddav permissions:
 | 
			
		||||
chown -f -R root.www-data ${RCM_PLUGIN_DIR}/carddav
 | 
			
		||||
# root.www-data need all permissions, others only read
 | 
			
		||||
chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
 | 
			
		||||
# root:www-data need all permissions, others only read
 | 
			
		||||
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
 | 
			
		||||
 | 
			
		||||
# Run Roundcube database migration script (database is created if it does not exist)
 | 
			
		||||
 | 
			
		||||
@ -397,7 +397,7 @@ miab_ldap_install() {
 | 
			
		||||
        need_pop="yes"
 | 
			
		||||
    fi
 | 
			
		||||
    
 | 
			
		||||
    H1 "MIAB-LDAP INSTALL [$(git describe 2>/dev/null)]"
 | 
			
		||||
    H1 "MIAB-LDAP INSTALL [$(pwd)] [$(git describe 2>/dev/null)]"
 | 
			
		||||
    # ensure we're in a MiaB-LDAP working directory
 | 
			
		||||
    if [ ! -e setup/ldap.sh ]; then
 | 
			
		||||
        die "Cannot install: the working directory is not MiaB-LDAP!"
 | 
			
		||||
@ -448,7 +448,7 @@ miab_ldap_install() {
 | 
			
		||||
    populate_by_cli_argument "$@"
 | 
			
		||||
    capture_state_by_cli_argument "$@"
 | 
			
		||||
    
 | 
			
		||||
    if [ "need_pop" = "yes" ]; then
 | 
			
		||||
    if [ "$need_pop" = "yes" ]; then
 | 
			
		||||
        popd >/dev/null
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,6 @@ popd >/dev/null
 | 
			
		||||
 | 
			
		||||
# install master miab-ldap and capture state
 | 
			
		||||
H2 "New miabldap"
 | 
			
		||||
echo "git branch: $(git branch | grep '*')"
 | 
			
		||||
miab_ldap_install --capture-state="/tmp/state/master"
 | 
			
		||||
 | 
			
		||||
# compare states
 | 
			
		||||
 | 
			
		||||
@ -110,7 +110,8 @@ except:
 | 
			
		||||
 | 
			
		||||
found = set()
 | 
			
		||||
buf = ""
 | 
			
		||||
input_lines = list(open(filename))
 | 
			
		||||
with open(filename, "r") as f:
 | 
			
		||||
	input_lines = list(f)
 | 
			
		||||
cur_section = None
 | 
			
		||||
 | 
			
		||||
while len(input_lines) > 0:
 | 
			
		||||
 | 
			
		||||
@ -49,8 +49,8 @@ cp "$1/owncloud.db" $STORAGE_ROOT/owncloud/
 | 
			
		||||
cp "$1/config.php" $STORAGE_ROOT/owncloud/
 | 
			
		||||
 | 
			
		||||
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
 | 
			
		||||
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
 | 
			
		||||
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
 | 
			
		||||
chown -f -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
 | 
			
		||||
chown www-data:www-data $STORAGE_ROOT/owncloud/config.php
 | 
			
		||||
 | 
			
		||||
sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ maintenance:mode --off
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,13 +26,8 @@ accesses = set()
 | 
			
		||||
# Scan the current and rotated access logs.
 | 
			
		||||
for fn in glob.glob("/var/log/nginx/access.log*"):
 | 
			
		||||
	# Gunzip if necessary.
 | 
			
		||||
	if fn.endswith(".gz"):
 | 
			
		||||
		f = gzip.open(fn)
 | 
			
		||||
	else:
 | 
			
		||||
		f = open(fn, "rb")
 | 
			
		||||
 | 
			
		||||
	# Loop through the lines in the access log.
 | 
			
		||||
	with f:
 | 
			
		||||
	with (gzip.open if fn.endswith(".gz") else open)(fn, "rb") as f:
 | 
			
		||||
		for line in f:
 | 
			
		||||
			# Find lines that are GETs on the bootstrap script by either curl or wget.
 | 
			
		||||
			# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
 | 
			
		||||
@ -52,7 +47,8 @@ for date, ip in accesses:
 | 
			
		||||
# Since logs are rotated, store the statistics permanently in a JSON file.
 | 
			
		||||
# Load in the stats from an existing file.
 | 
			
		||||
if os.path.exists(outfn):
 | 
			
		||||
	existing_data = json.load(open(outfn))
 | 
			
		||||
	with open(outfn, "r") as f:
 | 
			
		||||
		existing_data = json.load(f)
 | 
			
		||||
	for date, count in existing_data:
 | 
			
		||||
		if date not in by_date:
 | 
			
		||||
			by_date[date] = count
 | 
			
		||||
 | 
			
		||||
@ -133,13 +133,14 @@ def generate_documentation():
 | 
			
		||||
 """)
 | 
			
		||||
 | 
			
		||||
	parser = Source.parser()
 | 
			
		||||
	for line in open("setup/start.sh"):
 | 
			
		||||
		try:
 | 
			
		||||
			fn = parser.parse_string(line).filename()
 | 
			
		||||
		except:
 | 
			
		||||
			continue
 | 
			
		||||
		if fn in ("setup/start.sh", "setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"):
 | 
			
		||||
			continue
 | 
			
		||||
	with open("setup/start.sh", "r") as start_file: 
 | 
			
		||||
                for line in start_file:
 | 
			
		||||
                        try:
 | 
			
		||||
                                fn = parser.parse_string(line).filename()
 | 
			
		||||
                        except:
 | 
			
		||||
                                continue
 | 
			
		||||
                        if fn in ("setup/start.sh", "setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"):
 | 
			
		||||
                                continue
 | 
			
		||||
 | 
			
		||||
		import sys
 | 
			
		||||
		print(fn, file=sys.stderr)
 | 
			
		||||
@ -410,7 +411,8 @@ class BashScript(Grammar):
 | 
			
		||||
	@staticmethod
 | 
			
		||||
	def parse(fn):
 | 
			
		||||
		if fn in ("setup/functions.sh", "/etc/mailinabox.conf"): return ""
 | 
			
		||||
		string = open(fn).read()
 | 
			
		||||
		with open(fn, "r") as f:
 | 
			
		||||
			string = f.read()
 | 
			
		||||
 | 
			
		||||
		# tokenize
 | 
			
		||||
		string = re.sub(".* #NODOC\n", "", string)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user