diff --git a/.editorconfig b/.editorconfig index fbe828c3..85e5fd9c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,9 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.html] +indent_style = tab + [Makefile] indent_style = tab indent_size = 4 diff --git a/.gitignore b/.gitignore index 14e6c4a7..104111b7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ tools/__pycache__/ externals/ .env .vagrant -api/docs/api-docs.html \ No newline at end of file +api/docs/api-docs.html +mailinabox-ca.crt diff --git a/README.md b/README.md index 02445a20..ec41cc8e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,86 @@ -Mail-in-a-Box -============= +(Power) Mail-in-a-Box +===================== + +## Installation + +- **PRE-REQUISITES:** Debian 10 (Buster) or Ubuntu 20.04 LTS fresh installation + +Update packages: +```sh +sudo apt update +sudo apt full-upgrade +``` + +Make sure that the `en_US.UTF-8` locale exists and is set as primary (this depends on the image you use) +```sh +sudo apt install locales +sudo dpkg-reconfigure locales +``` + +Install Power-Mail-in-a-Box (short link) +```sh +curl -L https://dvn.pt/powermiab | sudo bash +``` + +If that doesn't work: +```sh +curl https://raw.githubusercontent.com/ddavness/power-mailinabox/master/setup/bootstrap.sh | sudo bash +``` + +## Current Version: v0.52.POWER.0 (Tracking v0.52) + +This is a fork of MiaB (duh), hacked and tuned to my needs: + +✅ - **Done** + +👨‍💻 - **Not there yet, but soon!** + +💤 - **I did not begin this part yet!** + +- ✅ Support for Debian AND Ubuntu 20.04 LTS; + +- ✅ Native support for SMTP relays (For example: SendGrid); + +- ✅ Bumped the bootstrap and jQuery dependencies' versions - and we've got a brand new admin panel now! + +- ✅ Per-domain `nginx` configuration: Custom pages will no longer have their pages defaulting to the MiaB services (`/admin`, `/mail`, etc.); + +- ✅ Updated NextCloud to the latest version available; + +- ✅ Performing backups immediately from the admin panel (independently from the daily schedule); + +- 👨‍💻 Encrypting backups using user-provided PGP keys; + +- 👨‍💻 Integrate a WKD server (Web Key Directory) for PGP keys; + +- 💤 Restricting access to the admin panel to certain IP's? + +- 💤 Customizing MTA names? (because privacy) + +### Ideas section: + +- 💤 Ability to download the backups from the admin panel; + +- 💤 Possibility of making some services optional (if they require more software to be installed) on setup? + +- - For example, one might simply not use NextCloud/Munin at all, and they're there... just wasting resources. + +- 💤 AXFR Transfers (for secondary DNS) using TSIG? + +- 💤 Expand DNS record options? + +- 💤 More complete webmail configuration via the admin panel/plugin management? + +- 💤 Expand the TOTP Two-Factor-Authentication for the webmail? + +- - Maybe U2F one day, too, but I don't have a capable device for this just yet... + +- 💤 Anything else I might need to use; + +All in all, I think I should rename this to something like "Central [Clown Computing](https://www.urbandictionary.com/define.php?term=clown%20computing)", since I'm trying to cram as many services as possible into that poor machine (Spending 5$ is better than spending 10$) + +Original Documentation +====================== By [@JoshData](https://github.com/JoshData) and [contributors](https://github.com/mail-in-a-box/mailinabox/graphs/contributors). @@ -15,7 +96,7 @@ Our goals are to: * Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web. * Have automated, auditable, and [idempotent](https://web.archive.org/web/20190518072631/https://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration. * **Not** make a totally unhackable, NSA-proof server. -* **Not** make something customizable by power users. +* ~~**Not** make something customizable by power users.~~ Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which supersedes the goals above. Please review it when joining our community. @@ -23,7 +104,7 @@ Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which su In The Box ---------- -Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components. +Mail-in-a-Box turns a fresh ~~Ubuntu 18.04 LTS~~ Debian 10 (Buster) 64-bit machine into a working mail server by installing and configuring various components. It is a one-click email appliance. There are no user-configurable setup options. It "just works." @@ -77,7 +158,7 @@ This is a challenge faced by everyone who runs their own mail server, with or wi Contributing and Development ---------------------------- -Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome. See [CONTRIBUTING](CONTRIBUTING.md) to get started. +Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome. See [CONTRIBUTING](CONTRIBUTING.md) to get started. The Acknowledgements diff --git a/Vagrantfile b/Vagrantfile index 467fb95e..6ed9dd80 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,8 +1,20 @@ + # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| - config.vm.box = "ubuntu/bionic64" + config.vm.box = "debian/buster64" + config.vm.provider :virtualbox do |vb| + vb.customize ["modifyvm", :id, "--cpus", 4, "--memory", 4096] + end + config.vm.provider :libvirt do |v| + v.memory = 4096 + v.cpus = 4 + v.nested = true + end + config.vm.provider :kvm do |kvm| + kvm.memory_size = '4096m' + end # Network config: Since it's a mail server, the machine must be connected # to the public web. However, we currently don't want to expose SSH since @@ -10,19 +22,20 @@ Vagrant.configure("2") do |config| # machine on a private network. config.vm.hostname = "mailinabox.lan" config.vm.network "private_network", ip: "192.168.50.4" + config.vm.synced_folder ".", "/vagrant", nfs_version: "3" + #, :mount_options => ["ro"] config.vm.provision :shell, :inline => <<-SH # Set environment variables so that the setup script does # not ask any questions during provisioning. We'll let the # machine figure out its own public IP. export NONINTERACTIVE=1 - export PUBLIC_IP=auto + export PUBLIC_IP=192.168.50.4 export PUBLIC_IPV6=auto export PRIMARY_HOSTNAME=auto - #export SKIP_NETWORK_CHECKS=1 - + export SKIP_NETWORK_CHECKS=1 # Start the setup script. cd /vagrant setup/start.sh SH -end +end \ No newline at end of file diff --git a/api/mailinabox.yml b/api/mailinabox.yml index 14cf54de..f5453f6f 100644 --- a/api/mailinabox.yml +++ b/api/mailinabox.yml @@ -499,6 +499,123 @@ paths: text/html: schema: type: string + /system/backup/new: + post: + tags: + - System + summary: Perform system backup + description: Performs a system backup. + operationId: performSystemBackup + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PerformBackupRequest' + examples: + incremental: + summary: Perform incremental backup. + value: + full: false + full: + summary: Force a full backup. + value: + full: true + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/backup/new" \ + -d "full=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/PerformBackupResponse' + 403: + description: Forbidden + content: + text/plain: + schema: + type: string + /system/smtp/relay: + get: + tags: + - System + summary: Get SMTP relay configuration + description: Gets basic configuration on how the box should use third-party relay services to deliver mail. + operationId: getRelayConfig + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/smtp/relay" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SmtpRelayConfig' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Set SMTP relay configuration + description: Sets the configuration on how the box should use third-party relays to deliver mail. + operationId: setRelayConfig + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SetSmtpRelayConfigRequest' + examples: + disable: + summary: Do not use relays. + value: + enabled: false + host: "" + auth_enabled: false + user: "" + key: "" + no_auth: + summary: Use a relay that does not require authentication. + value: + enabled: true + host: smtp.relay.net + auth_enabled: false + user: "" + key: "" + auth: + summary: Use a relay that requires authentication. + value: + enabled: true + host: smtp.relay.net + auth_enabled: true + user: someuser + key: key-or-password-here + responses: + 200: + description: Successful operation + content: + text/plain: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/status: get: tags: @@ -2459,6 +2576,19 @@ components: minimum: 1 example: 3 description: Backup config update request. + PerformBackupRequest: + type: object + required: + - full + properties: + full: + type: boolean + example: false + description: New backup type. + PerformBackupResponse: + type: string + example: OK + description: Backup creation response. SystemBackupConfigUpdateResponse: type: string example: OK @@ -2661,6 +2791,52 @@ components: type: string example: web updated description: Web update response. + SmtpRelayConfig: + type: object + required: + - enabled + - host + - auth_enabled + - user + properties: + enabled: + type: boolean + example: true + host: + type: string + example: sendgrid.net + auth_enabled: + type: boolean + example: true + user: + type: string + example: someuser + description: SMTP configuration details. + SetSmtpRelayConfigRequest: + type: object + required: + - enabled + - host + - auth_enabled + - user + - key + properties: + enabled: + type: boolean + example: true + host: + type: string + example: sendgrid.net + auth_enabled: + type: boolean + example: true + user: + type: string + example: apikey + key: + type: string + example: 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 + description: SMTP Configuration form MfaStatusResponse: type: object properties: diff --git a/conf/nginx-alldomains.conf b/conf/nginx-alldomains.conf index 4c81e3f3..95008f19 100644 --- a/conf/nginx-alldomains.conf +++ b/conf/nginx-alldomains.conf @@ -1,6 +1,6 @@ - # Expose this directory as static files. root $ROOT; - index index.html index.htm; + + # ADDITIONAL DIRECTIVES HERE location = /robots.txt { log_not_found off; @@ -25,30 +25,6 @@ alias /var/lib/mailinabox/mta-sts.txt; } - # Roundcube Webmail configuration. - rewrite ^/mail$ /mail/ redirect; - rewrite ^/mail/$ /mail/index.php; - location /mail/ { - index index.php; - alias /usr/local/lib/roundcubemail/; - } - location ~ /mail/config/.* { - # A ~-style location is needed to give this precedence over the next block. - return 403; - } - location ~ /mail/.*\.php { - # note: ~ has precendence over a regular location block - include fastcgi_params; - fastcgi_split_path_info ^/mail(/.*)()$; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name; - fastcgi_pass php-fpm; - - # Outgoing mail also goes through this endpoint, so increase the maximum - # file upload limit to match the corresponding Postfix limit. - client_max_body_size 128M; - } - # Z-Push (Microsoft Exchange ActiveSync) location /Microsoft-Server-ActiveSync { include /etc/nginx/fastcgi_params; @@ -68,9 +44,6 @@ fastcgi_pass php-fpm; } - - # ADDITIONAL DIRECTIVES HERE - # Disable viewing dotfiles (.htaccess, .svn, .git, etc.) # This block is placed at the end. Nginx's precedence rules means this block # takes precedence over all non-regex matches and only regex matches that diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf index 31bf0095..31e50d5b 100644 --- a/conf/nginx-primaryonly.conf +++ b/conf/nginx-primaryonly.conf @@ -1,3 +1,5 @@ + # ADDITIONAL DIRECTIVES HERE + # Control Panel # Proxy /admin to our Python based control panel daemon. It is # listening on IPv4 only so use an IP address and not 'localhost'. @@ -14,6 +16,30 @@ add_header Content-Security-Policy "frame-ancestors 'none';"; } + # Roundcube Webmail configuration. + rewrite ^/mail$ /mail/ redirect; + rewrite ^/mail/$ /mail/index.php; + location /mail/ { + index index.php; + alias /usr/local/lib/roundcubemail/; + } + location ~ /mail/config/.* { + # A ~-style location is needed to give this precedence over the next block. + return 403; + } + location ~ /mail/.*\.php { + # note: ~ has precendence over a regular location block + include fastcgi_params; + fastcgi_split_path_info ^/mail(/.*)()$; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name; + fastcgi_pass php-fpm; + + # Outgoing mail also goes through this endpoint, so increase the maximum + # file upload limit to match the corresponding Postfix limit. + client_max_body_size 128M; + } + # Nextcloud configuration. rewrite ^/cloud$ /cloud/ redirect; rewrite ^/cloud/$ /cloud/index.php; @@ -72,5 +98,3 @@ rewrite ^/.well-known/host-meta.json /cloud/public.php?service=host-meta-json last; rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect; rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect; - - # ADDITIONAL DIRECTIVES HERE diff --git a/conf/nginx-top.conf b/conf/nginx-top.conf index 4d888366..85d056cf 100644 --- a/conf/nginx-top.conf +++ b/conf/nginx-top.conf @@ -7,6 +7,5 @@ ## your own --- please do not ask for help from us. upstream php-fpm { - server unix:/var/run/php/php7.2-fpm.sock; + server unix:/var/run/php/php{{phpver}}-fpm.sock; } - diff --git a/conf/www_default.html b/conf/www_default.html index 68d0366b..e78b0bbc 100644 --- a/conf/www_default.html +++ b/conf/www_default.html @@ -1,10 +1,15 @@ - - this is a mail-in-a-box - - - -

this is a mail-in-a-box

-

take control of your email at https://mailinabox.email/

- + + + Redirecting... + + + + + + + + diff --git a/management/backup.py b/management/backup.py index 0a8a021e..f5773cab 100755 --- a/management/backup.py +++ b/management/backup.py @@ -10,9 +10,9 @@ import os, os.path, shutil, glob, re, datetime, sys import dateutil.parser, dateutil.relativedelta, dateutil.tz import rtyaml -from exclusiveprocess import Lock +from exclusiveprocess import Lock, CannotAcquireLock -from utils import load_environment, shell, wait_for_service, fix_boto +from utils import load_environment, shell, wait_for_service, fix_boto, get_php_version rsync_ssh_options = [ "--ssh-options= -i /root/.ssh/id_rsa_miab", @@ -20,7 +20,7 @@ rsync_ssh_options = [ ] def backup_status(env): - # If backups are dissbled, return no status. + # If backups are disabled, return no status. config = get_backup_config(env) if config["target"] == "off": return { } @@ -210,13 +210,22 @@ def get_target_type(config): protocol = config["target"].split(":")[0] return protocol -def perform_backup(full_backup): +def perform_backup(full_backup, user_initiated=False): env = load_environment() + php_fpm = f"php{get_php_version()}-fpm" # Create an global exclusive lock so that the backup script # cannot be run more than one. - Lock(die=True).forever() - + lock = Lock(name="mailinabox_backup_daemon", die=(not user_initiated)) + if user_initiated: + # God forgive me for what I'm about to do + try: + lock._acquire() + except CannotAcquireLock: + return "Another backup is already being done!" + else: + lock.forever() + config = get_backup_config(env) backup_root = os.path.join(env["STORAGE_ROOT"], 'backup') backup_cache_dir = os.path.join(backup_root, 'cache') @@ -247,7 +256,7 @@ def perform_backup(full_backup): if quit: sys.exit(code) - service_command("php7.2-fpm", "stop", quit=True) + service_command(php_fpm, "stop", quit=True) service_command("postfix", "stop", quit=True) service_command("dovecot", "stop", quit=True) @@ -281,7 +290,7 @@ def perform_backup(full_backup): # Start services again. service_command("dovecot", "start", quit=False) service_command("postfix", "start", quit=False) - service_command("php7.2-fpm", "start", quit=False) + service_command(php_fpm, "start", quit=False) # Remove old backups. This deletes all backup data no longer needed # from more than 3 days ago. @@ -329,8 +338,13 @@ def perform_backup(full_backup): # backup. Since it checks that dovecot and postfix are running, block for a # bit (maximum of 10 seconds each) to give each a chance to finish restarting # before the status checks might catch them down. See #381. - wait_for_service(25, True, env, 10) - wait_for_service(993, True, env, 10) + if user_initiated: + # God forgive me for what I'm about to do + lock._release() + # We don't need to wait for the services to be up in this case + else: + wait_for_service(25, True, env, 10) + wait_for_service(993, True, env, 10) def run_duplicity_verification(): env = load_environment() diff --git a/management/daemon.py b/management/daemon.py index a0cfefa6..d08dfdb3 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -16,7 +16,7 @@ from flask import Flask, request, render_template, abort, Response, send_from_di import auth, utils from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user -from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege +from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege, open_database from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa @@ -119,9 +119,12 @@ def index(): utils.fix_boto() # must call prior to importing boto import boto.s3 backup_s3_hosts = [(r.name, r.endpoint) for r in boto.s3.regions()] + lsb=utils.shell("check_output", ["/usr/bin/lsb_release", "-d"]) return render_template('index.html', hostname=env['PRIMARY_HOSTNAME'], + distname=lsb[lsb.find("\t")+1:-1], + storage_root=env['STORAGE_ROOT'], no_users_exist=no_users_exist, @@ -479,7 +482,10 @@ def web_get_domains(): @authorized_personnel_only def web_update(): from web_update import do_web_update - return do_web_update(env) + try: + return do_web_update(env) + except Exception as e: + return (str(e), 500) # System @@ -588,6 +594,19 @@ def backup_set_custom(): request.form.get('min_age', '') )) +@app.route('/system/backup/new', methods=["POST"]) +@authorized_personnel_only +def backup_new(): + from backup import perform_backup, get_backup_config + + # If backups are disabled, don't perform the backup + config = get_backup_config(env) + if config["target"] == "off": + return "Backups are disabled in this machine. Nothing was done." + + msg = perform_backup(request.form.get('full', False) == 'true', True) + return "OK" if msg is None else msg + @app.route('/system/privacy', methods=["GET"]) @authorized_personnel_only def privacy_status_get(): @@ -602,6 +621,49 @@ def privacy_status_set(): utils.write_settings(config, env) return "OK" +@app.route('/system/smtp/relay', methods=["GET"]) +@authorized_personnel_only +def smtp_relay_get(): + config = utils.load_settings(env) + return { + "enabled": config.get("SMTP_RELAY_ENABLED", True), + "host": config.get("SMTP_RELAY_HOST", ""), + "auth_enabled": config.get("SMTP_RELAY_AUTH", False), + "user": config.get("SMTP_RELAY_USER", "") + } + +@app.route('/system/smtp/relay', methods=["POST"]) +@authorized_personnel_only +def smtp_relay_set(): + from editconf import edit_conf + config = utils.load_settings(env) + newconf = request.form + try: + # Write on daemon settings + config["SMTP_RELAY_ENABLED"] = (newconf.get("enabled") == "true") + config["SMTP_RELAY_HOST"] = newconf.get("host") + config["SMTP_RELAY_AUTH"] = (newconf.get("auth_enabled") == "true") + config["SMTP_RELAY_USER"] = newconf.get("user") + utils.write_settings(config, env) + # Write on Postfix configs + edit_conf("/etc/postfix/main.cf", [ + "relayhost=" + (f"[{config['SMTP_RELAY_HOST']}]:587" if config["SMTP_RELAY_ENABLED"] else ""), + "smtp_sasl_auth_enable=" + ("yes" if config["SMTP_RELAY_AUTH"] else "no"), + "smtp_sasl_security_options=" + ("noanonymous" if config["SMTP_RELAY_AUTH"] else "anonymous"), + "smtp_sasl_tls_security_options=" + ("noanonymous" if config["SMTP_RELAY_AUTH"] else "anonymous") + ], delimiter_re=r"\s*=\s*", delimiter="=", comment_char="#") + if config["SMTP_RELAY_AUTH"]: + # Edit the sasl password + with open("/etc/postfix/sasl_passwd", "w") as f: + f.write(f"[{config['SMTP_RELAY_HOST']}]:587 {config['SMTP_RELAY_USER']}:{newconf.get('key')}\n") + utils.shell("check_output", ["/usr/bin/chmod", "600", "/etc/postfix/sasl_passwd"], capture_stderr=True) + utils.shell("check_output", ["/usr/sbin/postmap", "/etc/postfix/sasl_passwd"], capture_stderr=True) + # Restart Postfix + return utils.shell("check_output", ["/usr/bin/systemctl", "restart", "postfix"], capture_stderr=True) + except Exception as e: + return (str(e), 500) + + # MUNIN @app.route('/munin/') diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index db496399..dee8b602 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -12,14 +12,14 @@ export LC_TYPE=en_US.UTF-8 # On Mondays, i.e. once a week, send the administrator a report of total emails # sent and received so the admin might notice server abuse. if [ `date "+%u"` -eq 1 ]; then - management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report" + management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report" fi # Take a backup. management/backup.py 2>&1 | management/email_administrator.py "Backup Status" # Provision any new certificates for new domains or domains with expiring certificates. -management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result" +management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result" # Run status checks and email the administrator if anything changed. -management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice" +management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice" diff --git a/management/dns_update.py b/management/dns_update.py index 781fb1dc..6280414f 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -947,9 +947,9 @@ def get_secondary_dns(custom_dns, mode=None): # doesn't. if not hostname.startswith("xfr:"): if mode == "xfr": - response = dns.resolver.query(hostname+'.', "A", raise_on_no_answer=False) + response = dns.resolver.resolve(hostname+'.', "A", raise_on_no_answer=False) values.extend(map(str, response)) - response = dns.resolver.query(hostname+'.', "AAAA", raise_on_no_answer=False) + response = dns.resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False) values.extend(map(str, response)) continue values.append(hostname) @@ -972,7 +972,7 @@ def set_secondary_dns(hostnames, env): if not item.startswith("xfr:"): # Resolve hostname. try: - response = resolver.query(item, "A") + response = resolver.resolve(item, "A") except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): try: response = resolver.query(item, "AAAA") diff --git a/management/editconf.py b/management/editconf.py new file mode 100755 index 00000000..27bbd93e --- /dev/null +++ b/management/editconf.py @@ -0,0 +1,143 @@ +#!/usr/bin/python3 +# +# This is a helper tool for editing configuration files during the setup +# process. The tool is given new values for settings as command-line +# arguments. It comments-out existing setting values in the configuration +# file and adds new values either after their former location or at the +# end. +# +# The configuration file has settings that look like: +# +# NAME=VALUE +# +# If the -s option is given, then space becomes the delimiter, i.e.: +# +# NAME VALUE +# +# If the -c option is given, then the supplied character becomes the comment character +# +# If the -w option is given, then setting lines continue onto following +# lines while the lines start with whitespace, e.g.: +# +# NAME VAL +# UE + +# create the new config file in memory + +import sys, re + +def edit_conf(filename, settings, delimiter_re, delimiter, comment_char, folded_lines = False, testing = False): + found = set() + buf = "" + input_lines = list(open(filename, "r+")) + + while len(input_lines) > 0: + line = input_lines.pop(0) + + # If this configuration file uses folded lines, append any folded lines + # into our input buffer. + if folded_lines and line[0] not in (comment_char, " ", ""): + while len(input_lines) > 0 and input_lines[0][0] in " \t": + line += input_lines.pop(0) + + # See if this line is for any settings passed on the command line. + for i in range(len(settings)): + # Check that this line contain this setting from the command-line arguments. + name, val = settings[i].split("=", 1) + m = re.match( + "(\s*)" + + "(" + re.escape(comment_char) + "\s*)?" + + re.escape(name) + delimiter_re + "(.*?)\s*$", + line, re.S) + if not m: continue + indent, is_comment, existing_val = m.groups() + + # If this is already the setting, do nothing. + if is_comment is None and existing_val == val: + # It may be that we've already inserted this setting higher + # in the file so check for that first. + if i in found: break + buf += line + found.add(i) + break + + # comment-out the existing line (also comment any folded lines) + if is_comment is None: + buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n" + else: + # the line is already commented, pass it through + buf += line + + # if this option oddly appears more than once, don't add the setting again + if i in found: + break + + # add the new setting + buf += indent + name + delimiter + val + "\n" + + # note that we've applied this option + found.add(i) + + break + else: + # If did not match any setting names, pass this line through. + buf += line + + # Put any settings we didn't see at the end of the file. + for i in range(len(settings)): + if i not in found: + name, val = settings[i].split("=", 1) + buf += name + delimiter + val + "\n" + + if not testing: + # Write out the new file. + with open(filename, "w") as f: + f.write(buf) + else: + # Just print the new file to stdout. + print(buf) + +# Run standalone +if __name__ == "__main__": + # sanity check + if len(sys.argv) < 3: + print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c ] [-t] NAME=VAL [NAME=VAL ...]") + sys.exit(1) + + # parse command line arguments + filename = sys.argv[1] + settings = sys.argv[2:] + + delimiter = "=" + delimiter_re = r"\s*=\s*" + comment_char = "#" + folded_lines = False + testing = False + while settings[0][0] == "-" and settings[0] != "--": + opt = settings.pop(0) + if opt == "-s": + # Space is the delimiter + delimiter = " " + delimiter_re = r"\s+" + elif opt == "-w": + # Line folding is possible in this file. + folded_lines = True + elif opt == "-c": + # Specifies a different comment character. + comment_char = settings.pop(0) + elif opt == "-t": + testing = True + else: + print("Invalid option.") + sys.exit(1) + + # sanity check command line + for setting in settings: + try: + name, value = setting.split("=", 1) + except: + import subprocess + print("Invalid command line: ", subprocess.list2cmdline(sys.argv)) + sys.exit(1) + + edit_conf(filename, settings, delimiter_re, delimiter, comment_char, folded_lines, testing) diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 3e1b5856..96959425 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -346,6 +346,8 @@ def provision_certificates(env, limit_domains): "certonly", #"-v", # just enough to see ACME errors "--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup + "--agree-tos", # Automatically agrees to Let's Encrypt TOS + "--register-unsafely-without-email", # The daemon takes care of renewals "-d", ",".join(domain_list), # first will be main domain diff --git a/management/status_checks.py b/management/status_checks.py index 631a82a2..d3fb7ae5 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -280,9 +280,9 @@ def run_network_checks(env, output): if ret == 0: output.print_ok("Outbound mail (SMTP port 25) is not blocked.") else: - output.print_error("""Outbound mail (SMTP port 25) seems to be blocked by your network. You - will not be able to send any mail. Many residential networks block port 25 to prevent hijacked - machines from being able to send spam. A quick connection test to Google's mail server on port 25 + output.print_warning("""Outbound mail (SMTP port 25) seems to be blocked by your network. You + will not be able to send any mail without a SMTP relay. Many residential networks block port 25 to prevent + hijacked machines from being able to send spam. A quick connection test to Google's mail server on port 25 failed.""") # Stop if the IPv4 address is listed in the ZEN Spamhaus Block List. @@ -300,6 +300,19 @@ def run_network_checks(env, output): which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s.""" % (env['PUBLIC_IP'], zen, env['PUBLIC_IP'])) + # Check if a SMTP relay is set up. It's not strictly required, but on some providers + # it might be needed. + config = load_settings(env) + if config.get("SMTP_RELAY_ENABLED"): + if config.get("SMTP_RELAY_AUTH"): + output.print_ok("An authenticated SMTP relay has been set up via port 587.") + else: + output.print_warning("A SMTP relay has been set up, but it is not authenticated.") + elif ret == 0: + output.print_ok("No SMTP relay has been set up (but that's ok since port 25 is not blocked).") + else: + output.print_error("No SMTP relay has been set up. Since port 25 is blocked, you will probably not be able to send any mail.") + def run_domain_checks(rounded_time, env, output, pool): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) @@ -735,7 +748,7 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None): # Do the query. try: - response = resolver.query(qname, rtype) + response = resolver.resolve(qname, rtype) except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): # Host did not have an answer for this query; not sure what the # difference is between the two exceptions. @@ -834,7 +847,7 @@ def what_version_is_this(env): # Git may not be installed and Mail-in-a-Box may not have been cloned from github, # so this function may raise all sorts of exceptions. miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - tag = shell("check_output", ["/usr/bin/git", "describe", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() + tag = shell("check_output", ["/usr/bin/git", "describe", "--tags", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() return tag def get_latest_miab_version(): @@ -844,7 +857,7 @@ def get_latest_miab_version(): from socket import timeout try: - return re.search(b'TAG=(.*)', urlopen("https://mailinabox.email/setup.sh?ping=1", timeout=5).read()).group(1).decode("utf8") + return re.search(b'TAG=(.*)', urlopen("https://raw.githubusercontent.com/ddavness/power-mailinabox/master/setup/bootstrap.sh", timeout=5).read()).group(1).decode("utf8") except (HTTPError, URLError, timeout): return None diff --git a/management/templates/aliases.html b/management/templates/aliases.html index 848fcf49..1f60ab1f 100644 --- a/management/templates/aliases.html +++ b/management/templates/aliases.html @@ -94,10 +94,10 @@ - + - + diff --git a/management/templates/external-dns.html b/management/templates/external-dns.html index 0634ec82..d596e64a 100644 --- a/management/templates/external-dns.html +++ b/management/templates/external-dns.html @@ -36,7 +36,7 @@