1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-20 02:52:11 +00:00

Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Michael Kroes 2021-01-06 10:54:24 +01:00
commit 114fc4fbec
12 changed files with 220 additions and 28 deletions

View File

@ -1,6 +1,17 @@
CHANGELOG CHANGELOG
========= =========
In Development
--------------
* Incoming emails with SPF/DKIM/DMARC failures now have a higher spam score, and these messages are more likely to appear in the junk folder, since they are often spam/phishing.
* A new Download button in the control panel's External DNS page can be used to download the required DNS records in zonefile format.
* Backblaze B2 is now a supported backup protocol.
* Fixed the problem when the control panel would report DNS entries as Not Set by increasing a bind query limit.
* Fixed a control panel startup bug on some systems.
* Fixed the MTA-STS policy file's line endings.
* Nextcloud's photos, dashboard, and activity apps are disabled since we only support contacts and calendar.
v0.51 (November 14, 2020) v0.51 (November 14, 2020)
------------------------- -------------------------

View File

@ -15,7 +15,7 @@ info:
license: license:
name: CC0 1.0 Universal name: CC0 1.0 Universal
url: https://creativecommons.org/publicdomain/zero/1.0/legalcode url: https://creativecommons.org/publicdomain/zero/1.0/legalcode
version: 0.47.0 version: 0.51.0
x-logo: x-logo:
url: https://mailinabox.email/static/logo.png url: https://mailinabox.email/static/logo.png
altText: Mail-in-a-Box logo altText: Mail-in-a-Box logo
@ -743,6 +743,38 @@ paths:
text/html: text/html:
schema: schema:
type: string type: string
/dns/zonefile/{zone}:
parameters:
- in: path
name: zone
schema:
$ref: '#/components/schemas/Hostname'
required: true
description: Hostname
get:
tags:
- DNS
summary: Get DNS zonefile
description: Returns a DNS zone file for a hostname.
operationId: getDnsZonefile
x-codeSamples:
- lang: curl
source: |
curl -X GET "https://{host}/admin/dns/zonefile/<zone>" \
-u "<email>:<password>"
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/DNSZonefileResponse'
403:
description: Forbidden
content:
text/html:
schema:
type: string
/dns/update: /dns/update:
post: post:
tags: tags:
@ -1781,7 +1813,7 @@ components:
text/plain: text/plain:
schema: schema:
type: string type: string
example: 1.2.3.4 example: '1.2.3.4'
description: The value of the DNS record. description: The value of the DNS record.
example: '1.2.3.4' example: '1.2.3.4'
schemas: schemas:
@ -2050,6 +2082,8 @@ components:
items: items:
$ref: '#/components/schemas/Hostname' $ref: '#/components/schemas/Hostname'
description: DNS zones response. description: DNS zones response.
DNSZonefileResponse:
type: string
DNSSecondaryNameserverResponse: DNSSecondaryNameserverResponse:
type: object type: object
required: required:
@ -2663,13 +2697,6 @@ components:
type: string type: string
MfaEnableSuccessResponse: MfaEnableSuccessResponse:
type: string type: string
MfaEnableBadRequestResponse:
type: object
required:
- error
properties:
error:
type: string
MfaDisableRequest: MfaDisableRequest:
type: object type: object
properties: properties:

View File

@ -456,6 +456,23 @@ def list_target_files(config):
raise ValueError(e.reason) raise ValueError(e.reason)
return [(key.name[len(path):], key.size) for key in bucket.list(prefix=path)] return [(key.name[len(path):], key.size) for key in bucket.list(prefix=path)]
elif target.scheme == 'b2':
from b2sdk.v1 import InMemoryAccountInfo, B2Api
from b2sdk.v1.exception import NonExistentBucket
info = InMemoryAccountInfo()
b2_api = B2Api(info)
# Extract information from target
b2_application_keyid = target.netloc[:target.netloc.index(':')]
b2_application_key = target.netloc[target.netloc.index(':')+1:target.netloc.index('@')]
b2_bucket = target.netloc[target.netloc.index('@')+1:]
try:
b2_api.authorize_account("production", b2_application_keyid, b2_application_key)
bucket = b2_api.get_bucket_by_name(b2_bucket)
except NonExistentBucket as e:
raise ValueError("B2 Bucket does not exist. Please double check your information!")
return [(key.file_name, key.size) for key, _ in bucket.ls()]
else: else:
raise ValueError(config["target"]) raise ValueError(config["target"])

View File

@ -1,3 +1,12 @@
#!/usr/local/lib/mailinabox/env/bin/python3
#
# During development, you can start the Mail-in-a-Box control panel
# by running this script, e.g.:
#
# service mailinabox stop # stop the system process
# DEBUG=1 management/daemon.py
# service mailinabox start # when done debugging, start it up again
import os, os.path, re, json, time import os, os.path, re, json, time
import multiprocessing.pool, subprocess import multiprocessing.pool, subprocess
@ -338,6 +347,12 @@ def dns_get_dump():
from dns_update import build_recommended_dns from dns_update import build_recommended_dns
return json_response(build_recommended_dns(env)) return json_response(build_recommended_dns(env))
@app.route('/dns/zonefile/<zone>')
@authorized_personnel_only
def dns_get_zonefile(zone):
from dns_update import get_dns_zonefile
return Response(get_dns_zonefile(zone, env), status=200, mimetype='text/plain')
# SSL # SSL
@app.route('/ssl/status') @app.route('/ssl/status')
@ -672,7 +687,22 @@ def log_failed_login(request):
# APP # APP
if __name__ == '__main__': if __name__ == '__main__':
if "DEBUG" in os.environ: app.debug = True if "DEBUG" in os.environ:
# Turn on Flask debugging.
app.debug = True
# Use a stable-ish master API key so that login sessions don't restart on each run.
# Use /etc/machine-id to seed the key with a stable secret, but add something
# and hash it to prevent possibly exposing the machine id, using the time so that
# the key is not valid indefinitely.
import hashlib
with open("/etc/machine-id") as f:
api_key = f.read()
api_key += "|" + str(int(time.time() / (60*60*2)))
hasher = hashlib.sha1()
hasher.update(api_key.encode("ascii"))
auth_service.key = hasher.hexdigest()
if "APIKEY" in os.environ: auth_service.key = os.environ["APIKEY"] if "APIKEY" in os.environ: auth_service.key = os.environ["APIKEY"]
if not app.debug: if not app.debug:

View File

@ -470,14 +470,14 @@ def write_nsd_zone(domain, zonefile, records, env, force):
zone = """ zone = """
$ORIGIN {domain}. $ORIGIN {domain}.
$TTL 1800 ; default time to live $TTL 86400 ; default time to live
@ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. ( @ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. (
__SERIAL__ ; serial number __SERIAL__ ; serial number
7200 ; Refresh (secondary nameserver update interval) 7200 ; Refresh (secondary nameserver update interval)
1800 ; Retry (when refresh fails, how often to try again) 86400 ; Retry (when refresh fails, how often to try again)
1209600 ; Expire (when refresh fails, how long secondary nameserver will keep records around anyway) 1209600 ; Expire (when refresh fails, how long secondary nameserver will keep records around anyway)
1800 ; Negative TTL (how long negative responses are cached) 86400 ; Negative TTL (how long negative responses are cached)
) )
""" """
@ -564,6 +564,17 @@ $TTL 1800 ; default time to live
return True # file is updated return True # file is updated
def get_dns_zonefile(zone, env):
for domain, fn in get_dns_zones(env):
if zone == domain:
break
else:
raise ValueError("%s is not a domain name that corresponds to a zone." % zone)
nsd_zonefile = "/etc/nsd/zones/" + fn
with open(nsd_zonefile, "r") as f:
return f.read()
######################################################################## ########################################################################
def write_nsd_conf(zonefiles, additional_records, env): def write_nsd_conf(zonefiles, additional_records, env):

View File

@ -89,7 +89,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-1 col-sm-11"> <div class="col-sm-offset-1 col-sm-11">
<p class="small"> <p class="small">
Multiple secondary servers can be separated with commas or spaces (i.e., <code>ns2.hostingcompany.com ns3.hostingcompany.com</code>). Multiple secondary servers can be separated with commas or spaces (i.e., <code>ns2.hostingcompany.com ns3.hostingcompany.com</code>).
To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using <code>xfr:10.20.30.40</code> or <code>xfr:10.0.0.0/8</code>. To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using <code>xfr:10.20.30.40</code> or <code>xfr:10.0.0.0/8</code>.
</p> </p>
<p id="secondarydns-clear-instructions" style="display: none" class="small"> <p id="secondarydns-clear-instructions" style="display: none" class="small">

View File

@ -42,6 +42,19 @@
You may need to adopt this technique when adding DomainKeys. Use a tool like <code>named-checkzone</code> to validate your zone file. You may need to adopt this technique when adding DomainKeys. Use a tool like <code>named-checkzone</code> to validate your zone file.
</p> </p>
<h3>Download zonefile</h3>
<p>You can download your zonefiles here or use the table of records below.</p>
<form class="form-inline" role="form" onsubmit="do_download_zonefile(); return false;">
<div class="form-group">
<div class="form-group">
<label for="downloadZonefile" class="control-label sr-only">Zone</label>
<select id="downloadZonefile" class="form-control" style="width: auto"> </select>
</div>
<button type="submit" class="btn btn-primary">Download</button>
</div>
</form>
<h3>Records</h3>
<table id="external_dns_settings" class="table"> <table id="external_dns_settings" class="table">
<thead> <thead>
@ -57,6 +70,18 @@
<script> <script>
function show_external_dns() { function show_external_dns() {
api(
"/dns/zones",
"GET",
{ },
function(data) {
var zones = $('#downloadZonefile');
zones.text('');
for (var j = 0; j < data.length; j++) {
zones.append($('<option/>').text(data[j]));
}
});
$('#external_dns_settings tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>") $('#external_dns_settings tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api( api(
"/dns/dump", "/dns/dump",
@ -84,4 +109,19 @@ function show_external_dns() {
} }
}) })
} }
function do_download_zonefile() {
var zone = $('#downloadZonefile').val();
api(
"/dns/zonefile/"+ zone,
"GET",
{},
function(data) {
show_modal_error("Download Zonefile", $("<pre/>").text(data));
},
function(err) {
show_modal_error("Download Zonefile (Error)", $("<pre/>").text(err));
});
}
</script> </script>

View File

@ -18,6 +18,7 @@
<option value="local">{{hostname}}</option> <option value="local">{{hostname}}</option>
<option value="rsync">rsync</option> <option value="rsync">rsync</option>
<option value="s3">Amazon S3</option> <option value="s3">Amazon S3</option>
<option value="b2">Backblaze B2</option>
</select> </select>
</div> </div>
</div> </div>
@ -111,6 +112,31 @@
<input type="text" class="form-control" rows="1" id="backup-target-pass"> <input type="text" class="form-control" rows="1" id="backup-target-pass">
</div> </div>
</div> </div>
<!-- Backblaze -->
<div class="form-group backup-target-b2">
<div class="col-sm-10 col-sm-offset-2">
<p>Backups are stored in a <a href="https://www.backblaze.com/" target="_blank" rel="noreferrer">Backblaze</a> B2 bucket. You must have a Backblaze account already.</p>
<p>You MUST manually copy the encryption password from <tt class="backup-encpassword-file"></tt> to a safe and secure location. You will need this file to decrypt backup files. It is NOT stored in your Backblaze B2 bucket.</p>
</div>
</div>
<div class="form-group backup-target-b2">
<label for="backup-target-b2-user" class="col-sm-2 control-label">B2 Application KeyID</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-b2-user">
</div>
</div>
<div class="form-group backup-target-b2">
<label for="backup-target-b2-pass" class="col-sm-2 control-label">B2 Application Key</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-b2-pass">
</div>
</div>
<div class="form-group backup-target-b2">
<label for="backup-target-b2-bucket" class="col-sm-2 control-label">B2 Bucket</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-b2-bucket">
</div>
</div>
<!-- Common --> <!-- Common -->
<div class="form-group backup-target-local backup-target-rsync backup-target-s3"> <div class="form-group backup-target-local backup-target-rsync backup-target-s3">
<label for="min-age" class="col-sm-2 control-label">Retention Days:</label> <label for="min-age" class="col-sm-2 control-label">Retention Days:</label>
@ -144,7 +170,7 @@
function toggle_form() { function toggle_form() {
var target_type = $("#backup-target-type").val(); var target_type = $("#backup-target-type").val();
$(".backup-target-local, .backup-target-rsync, .backup-target-s3").hide(); $(".backup-target-local, .backup-target-rsync, .backup-target-s3, .backup-target-b2").hide();
$(".backup-target-" + target_type).show(); $(".backup-target-" + target_type).show();
init_inputs(target_type); init_inputs(target_type);
@ -215,7 +241,7 @@ function show_system_backup() {
} }
function show_custom_backup() { function show_custom_backup() {
$(".backup-target-local, .backup-target-rsync, .backup-target-s3").hide(); $(".backup-target-local, .backup-target-rsync, .backup-target-s3, .backup-target-b2").hide();
api( api(
"/system/backup/config", "/system/backup/config",
"GET", "GET",
@ -245,6 +271,15 @@ function show_custom_backup() {
var host = hostpath.shift(); var host = hostpath.shift();
$("#backup-target-s3-host").val(host); $("#backup-target-s3-host").val(host);
$("#backup-target-s3-path").val(hostpath.join('/')); $("#backup-target-s3-path").val(hostpath.join('/'));
} else if (r.target.substring(0, 5) == "b2://") {
$("#backup-target-type").val("b2");
var targetPath = r.target.substring(5);
var b2_application_keyid = targetPath.split(':')[0];
var b2_applicationkey = targetPath.split(':')[1].split('@')[0];
var b2_bucket = targetPath.split('@')[1];
$("#backup-target-b2-user").val(b2_application_keyid);
$("#backup-target-b2-pass").val(b2_applicationkey);
$("#backup-target-b2-bucket").val(b2_bucket);
} }
toggle_form() toggle_form()
}) })
@ -264,6 +299,11 @@ function set_custom_backup() {
target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val() target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val()
+ "/" + $("#backup-target-rsync-path").val(); + "/" + $("#backup-target-rsync-path").val();
target_user = ''; target_user = '';
} else if (target_type == "b2") {
target = 'b2://' + $('#backup-target-b2-user').val() + ':' + $('#backup-target-b2-pass').val()
+ '@' + $('#backup-target-b2-bucket').val()
target_user = '';
target_pass = '';
} }
@ -303,4 +343,4 @@ function init_inputs(target_type) {
set_host($('#backup-target-s3-host-select').val()); set_host($('#backup-target-s3-host-select').val());
} }
} }
</script> </script>

View File

@ -31,7 +31,7 @@
<button type="submit" class="btn btn-primary">Add User</button> <button type="submit" class="btn btn-primary">Add User</button>
</form> </form>
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;"> <ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
<li>Passwords must be at least eight characters consisting of English lettters and numbers only. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li> <li>Passwords must be at least eight characters consisting of English letters and numbers only. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li>
<li>Use <a href="#" onclick="return show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.</li> <li>Use <a href="#" onclick="return show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.</li>
<li>Administrators get access to this control panel.</li> <li>Administrators get access to this control panel.</li>
<li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#" onclick="return show_panel('aliases');">aliases</a> can.</li> <li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#" onclick="return show_panel('aliases');">aliases</a> can.</li>

View File

@ -18,11 +18,7 @@ while [ -d /usr/local/lib/python3.4/dist-packages/acme ]; do
pip3 uninstall -y acme; pip3 uninstall -y acme;
done done
# duplicity is used to make backups of user data. It uses boto # duplicity is used to make backups of user data.
# (via Python 2) to do backups to AWS S3. boto from the Ubuntu
# package manager is too out-of-date -- it doesn't support the newer
# S3 api used in some regions, which breaks backups to those regions.
# See #627, #653.
# #
# virtualenv is used to isolate the Python 3 packages we # virtualenv is used to isolate the Python 3 packages we
# install via pip from the system-installed packages. # install via pip from the system-installed packages.
@ -30,7 +26,11 @@ done
# certbot installs EFF's certbot which we use to # certbot installs EFF's certbot which we use to
# provision free TLS certificates. # provision free TLS certificates.
apt_install duplicity python-pip virtualenv certbot apt_install duplicity python-pip virtualenv certbot
hide_output pip2 install --upgrade boto
# b2sdk is used for backblaze backups.
# boto is used for amazon aws backups.
# Both are installed outside the pipenv, so they can be used by duplicity
hide_output pip3 install --upgrade b2sdk boto
# Create a virtualenv for the installation of Python 3 packages # Create a virtualenv for the installation of Python 3 packages
# used by the management daemon. # used by the management daemon.
@ -50,8 +50,8 @@ hide_output $venv/bin/pip install --upgrade pip
hide_output $venv/bin/pip install --upgrade \ hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \ rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil \ flask dnspython python-dateutil \
qrcode[pil] pyotp \ qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver "idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver b2sdk
# CONFIGURATION # CONFIGURATION
@ -90,6 +90,12 @@ rm -f /tmp/bootstrap.zip
# running after a reboot. # running after a reboot.
cat > $inst_dir/start <<EOF; cat > $inst_dir/start <<EOF;
#!/bin/bash #!/bin/bash
# Set character encoding flags to ensure that any non-ASCII don't cause problems.
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8
source $venv/bin/activate source $venv/bin/activate
exec python `pwd`/management/daemon.py exec python `pwd`/management/daemon.py
EOF EOF

View File

@ -93,6 +93,9 @@ hide_output add-apt-repository -y universe
# Install the certbot PPA. # Install the certbot PPA.
hide_output add-apt-repository -y ppa:certbot/certbot hide_output add-apt-repository -y ppa:certbot/certbot
# Install the duplicity PPA.
hide_output add-apt-repository -y ppa:duplicity-team/duplicity-release-git
# ### Update Packages # ### Update Packages
# Update system packages to make sure we have the latest upstream versions # Update system packages to make sure we have the latest upstream versions
@ -317,6 +320,9 @@ fi #NODOC
# name server, on IPV6. # name server, on IPV6.
# * The listen-on directive in named.conf.options restricts `bind9` to # * The listen-on directive in named.conf.options restricts `bind9` to
# binding to the loopback interface instead of all interfaces. # binding to the loopback interface instead of all interfaces.
# * The max-recursion-queries directive increases the maximum number of iterative queries.
# If more queries than specified are sent, bind9 returns SERVFAIL. After flushing the cache during system checks,
# we ran into the limit thus we are increasing it from 75 (default value) to 100.
apt_install bind9 apt_install bind9
tools/editconf.py /etc/default/bind9 \ tools/editconf.py /etc/default/bind9 \
"OPTIONS=\"-u bind -4\"" "OPTIONS=\"-u bind -4\""
@ -324,6 +330,10 @@ if ! grep -q "listen-on " /etc/bind/named.conf.options; then
# Add a listen-on directive if it doesn't exist inside the options block. # Add a listen-on directive if it doesn't exist inside the options block.
sed -i "s/^}/\n\tlisten-on { 127.0.0.1; };\n}/" /etc/bind/named.conf.options sed -i "s/^}/\n\tlisten-on { 127.0.0.1; };\n}/" /etc/bind/named.conf.options
fi fi
if ! grep -q "max-recursion-queries " /etc/bind/named.conf.options; then
# Add a max-recursion-queries directive if it doesn't exist inside the options block.
sed -i "s/^}/\n\tmax-recursion-queries 100;\n}/" /etc/bind/named.conf.options
fi
# First we'll disable systemd-resolved's management of resolv.conf and its stub server. # First we'll disable systemd-resolved's management of resolv.conf and its stub server.
# Breaking the symlink to /run/systemd/resolve/stub-resolv.conf means # Breaking the symlink to /run/systemd/resolve/stub-resolv.conf means

View File

@ -28,8 +28,8 @@ apt_install \
# Install Roundcube from source if it is not already present or if it is out of date. # Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of plugins to track # Combine the Roundcube version number with the commit hash of plugins to track
# whether we have the latest version of everything. # whether we have the latest version of everything.
VERSION=1.4.9 VERSION=1.4.10
HASH=df650f4d3eae9eaae2d5a5f06d68665691daf57d HASH=36b2351030e1ebddb8e39190d7b0ba82b1bbec1b
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5 HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
CARDDAV_VERSION=3.0.3 CARDDAV_VERSION=3.0.3