mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-12 17:07:23 +01:00
Merge tag 'v0.53' of https://github.com/mail-in-a-box/mailinabox
v0.53 (April 12, 2021) ---------------------- Software updates: * Upgraded Roundcube to version 1.4.11 addressing a security issue, and its desktop notifications plugin. * Upgraded Z-Push (for Exchange/ActiveSync) to version 2.6.2. Control panel: * Backblaze B2 is now a supported backup protocol. * Fixed an issue in the daily mail reports. * Sort the Custom DNS by zone and qname, and add an option to go back to the old sort order (creation order). Mail: * Enable sending DMARC failure reports to senders that request them. Setup: * Fixed error when upgrading from Nextcloud 13.
This commit is contained in:
@@ -456,6 +456,23 @@ def list_target_files(config):
|
||||
raise ValueError(e.reason)
|
||||
|
||||
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:
|
||||
raise ValueError(config["target"])
|
||||
|
||||
@@ -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 multiprocessing.pool, subprocess
|
||||
|
||||
@@ -292,17 +301,50 @@ def dns_set_secondary_nameserver():
|
||||
@app.route('/dns/custom')
|
||||
@authorized_personnel_only
|
||||
def dns_get_records(qname=None, rtype=None):
|
||||
from dns_update import get_custom_dns_config
|
||||
return json_response([
|
||||
{
|
||||
"qname": r[0],
|
||||
"rtype": r[1],
|
||||
"value": r[2],
|
||||
}
|
||||
for r in get_custom_dns_config(env)
|
||||
if r[0] != "_secondary_nameserver"
|
||||
and (not qname or r[0] == qname)
|
||||
and (not rtype or r[1] == rtype) ])
|
||||
# Get the current set of custom DNS records.
|
||||
from dns_update import get_custom_dns_config, get_dns_zones
|
||||
records = get_custom_dns_config(env, only_real_records=True)
|
||||
|
||||
# Filter per the arguments for the more complex GET routes below.
|
||||
records = [r for r in records
|
||||
if (not qname or r[0] == qname)
|
||||
and (not rtype or r[1] == rtype) ]
|
||||
|
||||
# Make a better data structure.
|
||||
records = [
|
||||
{
|
||||
"qname": r[0],
|
||||
"rtype": r[1],
|
||||
"value": r[2],
|
||||
"sort-order": { },
|
||||
} for r in records ]
|
||||
|
||||
# To help with grouping by zone in qname sorting, label each record with which zone it is in.
|
||||
# There's an inconsistency in how we handle zones in get_dns_zones and in sort_domains, so
|
||||
# do this first before sorting the domains within the zones.
|
||||
zones = utils.sort_domains([z[0] for z in get_dns_zones(env)], env)
|
||||
for r in records:
|
||||
for z in zones:
|
||||
if r["qname"] == z or r["qname"].endswith("." + z):
|
||||
r["zone"] = z
|
||||
break
|
||||
|
||||
# Add sorting information. The 'created' order follows the order in the YAML file on disk,
|
||||
# which tracs the order entries were added in the control panel since we append to the end.
|
||||
# The 'qname' sort order sorts by our standard domain name sort (by zone then by qname),
|
||||
# then by rtype, and last by the original order in the YAML file (since sorting by value
|
||||
# may not make sense, unless we parse IP addresses, for example).
|
||||
for i, r in enumerate(records):
|
||||
r["sort-order"]["created"] = i
|
||||
domain_sort_order = utils.sort_domains([r["qname"] for r in records], env)
|
||||
for i, r in enumerate(sorted(records, key = lambda r : (
|
||||
zones.index(r["zone"]),
|
||||
domain_sort_order.index(r["qname"]),
|
||||
r["rtype"]))):
|
||||
r["sort-order"]["qname"] = i
|
||||
|
||||
# Return.
|
||||
return json_response(records)
|
||||
|
||||
@app.route('/dns/custom/<qname>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
@app.route('/dns/custom/<qname>/<rtype>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
@@ -362,6 +404,12 @@ def dns_get_dump():
|
||||
from dns_update import build_recommended_dns
|
||||
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
|
||||
|
||||
@app.route('/ssl/status')
|
||||
@@ -719,7 +767,22 @@ def log_failed_login(request):
|
||||
# APP
|
||||
|
||||
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 not app.debug:
|
||||
|
||||
@@ -470,14 +470,14 @@ def write_nsd_zone(domain, zonefile, records, env, force):
|
||||
|
||||
zone = """
|
||||
$ORIGIN {domain}.
|
||||
$TTL 1800 ; default time to live
|
||||
$TTL 86400 ; default time to live
|
||||
|
||||
@ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. (
|
||||
__SERIAL__ ; serial number
|
||||
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)
|
||||
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
|
||||
|
||||
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):
|
||||
@@ -742,7 +753,7 @@ def write_opendkim_tables(domains, env):
|
||||
|
||||
########################################################################
|
||||
|
||||
def get_custom_dns_config(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')))
|
||||
if not isinstance(custom_dns, dict): raise ValueError() # caught below
|
||||
@@ -750,6 +761,8 @@ def get_custom_dns_config(env):
|
||||
return [ ]
|
||||
|
||||
for qname, value in custom_dns.items():
|
||||
if qname == "_secondary_nameserver" and only_real_records: continue # skip fake record
|
||||
|
||||
# Short form. Mapping a domain name to a string is short-hand
|
||||
# for creating A records.
|
||||
if isinstance(value, str):
|
||||
|
||||
@@ -44,9 +44,8 @@ TIME_DELTAS = OrderedDict([
|
||||
('today', datetime.datetime.now() - datetime.datetime.now().replace(hour=0, minute=0, second=0))
|
||||
])
|
||||
|
||||
# Start date > end date!
|
||||
START_DATE = datetime.datetime.now()
|
||||
END_DATE = None
|
||||
END_DATE = NOW = datetime.datetime.now()
|
||||
START_DATE = None
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
@@ -121,7 +120,7 @@ def scan_mail_log(env):
|
||||
pass
|
||||
|
||||
print("Scanning logs from {:%Y-%m-%d %H:%M:%S} to {:%Y-%m-%d %H:%M:%S}".format(
|
||||
END_DATE, START_DATE)
|
||||
START_DATE, END_DATE)
|
||||
)
|
||||
|
||||
# Scan the lines in the log files until the date goes out of range
|
||||
@@ -253,7 +252,7 @@ def scan_mail_log(env):
|
||||
|
||||
if collector["postgrey"]:
|
||||
msg = "Greylisted Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
|
||||
print_header(msg.format(END_DATE, START_DATE))
|
||||
print_header(msg.format(START_DATE, END_DATE))
|
||||
|
||||
print(textwrap.fill(
|
||||
"The following mail was greylisted, meaning the emails were temporarily rejected. "
|
||||
@@ -291,7 +290,7 @@ def scan_mail_log(env):
|
||||
|
||||
if collector["rejected"]:
|
||||
msg = "Blocked Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
|
||||
print_header(msg.format(END_DATE, START_DATE))
|
||||
print_header(msg.format(START_DATE, END_DATE))
|
||||
|
||||
data = OrderedDict(sorted(collector["rejected"].items(), key=email_sort))
|
||||
|
||||
@@ -344,20 +343,20 @@ def scan_mail_log_line(line, collector):
|
||||
|
||||
# Replaced the dateutil parser for a less clever way of parser that is roughly 4 times faster.
|
||||
# date = dateutil.parser.parse(date)
|
||||
|
||||
# date = datetime.datetime.strptime(date, '%b %d %H:%M:%S')
|
||||
# date = date.replace(START_DATE.year)
|
||||
|
||||
# strptime fails on Feb 29 if correct year is not provided. See https://bugs.python.org/issue26460
|
||||
date = datetime.datetime.strptime(str(START_DATE.year) + ' ' + date, '%Y %b %d %H:%M:%S')
|
||||
# print("date:", date)
|
||||
|
||||
# strptime fails on Feb 29 with ValueError: day is out of range for month if correct year is not provided.
|
||||
# See https://bugs.python.org/issue26460
|
||||
date = datetime.datetime.strptime(str(NOW.year) + ' ' + date, '%Y %b %d %H:%M:%S')
|
||||
# if log date in future, step back a year
|
||||
if date > NOW:
|
||||
date = date.replace(year = NOW.year - 1)
|
||||
#print("date:", date)
|
||||
|
||||
# Check if the found date is within the time span we are scanning
|
||||
# END_DATE < START_DATE
|
||||
if date > START_DATE:
|
||||
if date > END_DATE:
|
||||
# Don't process, and halt
|
||||
return False
|
||||
elif date < END_DATE:
|
||||
elif date < START_DATE:
|
||||
# Don't process, but continue
|
||||
return True
|
||||
|
||||
@@ -606,7 +605,7 @@ def email_sort(email):
|
||||
|
||||
|
||||
def valid_date(string):
|
||||
""" Validate the given date string fetched from the --startdate argument """
|
||||
""" Validate the given date string fetched from the --enddate argument """
|
||||
try:
|
||||
date = dateutil.parser.parse(string)
|
||||
except ValueError:
|
||||
@@ -820,12 +819,14 @@ if __name__ == "__main__":
|
||||
|
||||
parser.add_argument("-t", "--timespan", choices=TIME_DELTAS.keys(), default='today',
|
||||
metavar='<time span>',
|
||||
help="Time span to scan, going back from the start date. Possible values: "
|
||||
help="Time span to scan, going back from the end date. Possible values: "
|
||||
"{}. Defaults to 'today'.".format(", ".join(list(TIME_DELTAS.keys()))))
|
||||
parser.add_argument("-d", "--startdate", action="store", dest="startdate",
|
||||
type=valid_date, metavar='<start date>',
|
||||
help="Date and time to start scanning the log file from. If no date is "
|
||||
"provided, scanning will start from the current date and time.")
|
||||
# keep the --startdate arg for backward compatibility
|
||||
parser.add_argument("-d", "--enddate", "--startdate", action="store", dest="enddate",
|
||||
type=valid_date, metavar='<end date>',
|
||||
help="Date and time to end scanning the log file. If no date is "
|
||||
"provided, scanning will end at the current date and time. "
|
||||
"Alias --startdate is for compatibility.")
|
||||
parser.add_argument("-u", "--users", action="store", dest="users",
|
||||
metavar='<email1,email2,email...>',
|
||||
help="Comma separated list of (partial) email addresses to filter the "
|
||||
@@ -837,13 +838,13 @@ if __name__ == "__main__":
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.startdate is not None:
|
||||
START_DATE = args.startdate
|
||||
if args.enddate is not None:
|
||||
END_DATE = args.enddate
|
||||
if args.timespan == 'today':
|
||||
args.timespan = 'day'
|
||||
print("Setting start date to {}".format(START_DATE))
|
||||
print("Setting end date to {}".format(END_DATE))
|
||||
|
||||
END_DATE = START_DATE - TIME_DELTAS[args.timespan]
|
||||
START_DATE = END_DATE - TIME_DELTAS[args.timespan]
|
||||
|
||||
VERBOSE = args.verbose
|
||||
|
||||
|
||||
@@ -293,6 +293,8 @@ def run_network_checks(env, output):
|
||||
zen = query_dns(rev_ip4+'.zen.spamhaus.org', 'A', nxdomain=None)
|
||||
if zen is None:
|
||||
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.")
|
||||
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."""
|
||||
@@ -678,6 +680,8 @@ def check_mail_domain(domain, env, output):
|
||||
dbl = query_dns(domain+'.dbl.spamhaus.org', "A", nxdomain=None)
|
||||
if dbl is None:
|
||||
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))
|
||||
else:
|
||||
output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s),
|
||||
which may prevent recipients from receiving your mail.
|
||||
|
||||
@@ -153,8 +153,8 @@ function show_aliases() {
|
||||
function(r) {
|
||||
$('#alias_table tbody').html("");
|
||||
for (var i = 0; i < r.length; i++) {
|
||||
var hdr = $("<tr><td colspan='3'><h4/></td></tr>");
|
||||
hdr.find('h4').text(r[i].domain);
|
||||
var hdr = $("<tr><th colspan='4' style='background-color: #EEE'></th></tr>");
|
||||
hdr.find('th').text(r[i].domain);
|
||||
$('#alias_table tbody').append(hdr);
|
||||
|
||||
for (var k = 0; k < r[i].aliases.length; k++) {
|
||||
|
||||
@@ -57,7 +57,13 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table id="custom-dns-current" class="table" style="width: auto; display: none">
|
||||
<div style="text-align: right; font-size; 90%; margin-top: 1em;">
|
||||
sort by:
|
||||
<a href="#" onclick="window.miab_custom_dns_data_sort_order='qname'; show_current_custom_dns_update_after_sort(); return false;">domain name</a>
|
||||
|
|
||||
<a href="#" onclick="window.miab_custom_dns_data_sort_order='created'; show_current_custom_dns_update_after_sort(); return false;">created</a>
|
||||
</div>
|
||||
<table id="custom-dns-current" class="table" style="width: auto; display: none; margin-top: 0;">
|
||||
<thead>
|
||||
<th>Domain Name</th>
|
||||
<th>Record Type</th>
|
||||
@@ -89,7 +95,7 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-1 col-sm-11">
|
||||
<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>.
|
||||
</p>
|
||||
<p id="secondarydns-clear-instructions" style="display: none" class="small">
|
||||
@@ -192,36 +198,38 @@ function show_current_custom_dns() {
|
||||
$('#custom-dns-current').fadeIn();
|
||||
else
|
||||
$('#custom-dns-current').fadeOut();
|
||||
|
||||
var reverse_fqdn = function(el) {
|
||||
el.qname = el.qname.split('.').reverse().join('.');
|
||||
return el;
|
||||
}
|
||||
var sort = function(a, b) {
|
||||
if(a.qname === b.qname) {
|
||||
if(a.rtype === b.rtype) {
|
||||
return a.value > b.value ? 1 : -1;
|
||||
}
|
||||
return a.rtype > b.rtype ? 1 : -1;
|
||||
}
|
||||
return a.qname > b.qname ? 1 : -1;
|
||||
}
|
||||
window.miab_custom_dns_data = data;
|
||||
show_current_custom_dns_update_after_sort();
|
||||
});
|
||||
}
|
||||
|
||||
data = data.map(reverse_fqdn).sort(sort).map(reverse_fqdn);
|
||||
function show_current_custom_dns_update_after_sort() {
|
||||
var data = window.miab_custom_dns_data;
|
||||
var sort_key = window.miab_custom_dns_data_sort_order || "qname";
|
||||
|
||||
$('#custom-dns-current').find("tbody").text('');
|
||||
data.sort(function(a, b) { return a["sort-order"][sort_key] - b["sort-order"][sort_key] });
|
||||
|
||||
var tbody = $('#custom-dns-current').find("tbody");
|
||||
tbody.text('');
|
||||
var last_zone = null;
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (sort_key == "qname" && data[i].zone != last_zone) {
|
||||
var r = $("<tr><th colspan=4 style='background-color: #EEE'></th></tr>");
|
||||
r.find("th").text(data[i].zone);
|
||||
tbody.append(r);
|
||||
last_zone = data[i].zone;
|
||||
}
|
||||
|
||||
var tr = $("<tr/>");
|
||||
$('#custom-dns-current').find("tbody").append(tr);
|
||||
tbody.append(tr);
|
||||
tr.attr('data-qname', data[i].qname);
|
||||
tr.attr('data-rtype', data[i].rtype);
|
||||
tr.attr('data-value', data[i].value);
|
||||
tr.append($('<td class="long"/>').text(data[i].qname));
|
||||
tr.append($('<td/>').text(data[i].rtype));
|
||||
tr.append($('<td class="long"/>').text(data[i].value));
|
||||
tr.append($('<td class="long" style="max-width: 40em"/>').text(data[i].value));
|
||||
tr.append($('<td>[<a href="#" onclick="return delete_custom_dns_record(this)">delete</a>]</td>'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delete_custom_dns_record(elem) {
|
||||
|
||||
@@ -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.
|
||||
</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">
|
||||
<thead>
|
||||
@@ -57,6 +70,18 @@
|
||||
|
||||
<script>
|
||||
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>")
|
||||
api(
|
||||
"/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>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<option value="local">{{hostname}}</option>
|
||||
<option value="rsync">rsync</option>
|
||||
<option value="s3">Amazon S3</option>
|
||||
<option value="b2">Backblaze B2</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,6 +112,31 @@
|
||||
<input type="text" class="form-control" rows="1" id="backup-target-pass">
|
||||
</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 -->
|
||||
<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>
|
||||
@@ -144,7 +170,7 @@
|
||||
|
||||
function toggle_form() {
|
||||
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();
|
||||
|
||||
init_inputs(target_type);
|
||||
@@ -215,7 +241,7 @@ function show_system_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(
|
||||
"/system/backup/config",
|
||||
"GET",
|
||||
@@ -245,6 +271,15 @@ function show_custom_backup() {
|
||||
var host = hostpath.shift();
|
||||
$("#backup-target-s3-host").val(host);
|
||||
$("#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()
|
||||
})
|
||||
@@ -264,6 +299,11 @@ function set_custom_backup() {
|
||||
target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val()
|
||||
+ "/" + $("#backup-target-rsync-path").val();
|
||||
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());
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,7 +1,6 @@
|
||||
<h2>Users</h2>
|
||||
|
||||
<style>
|
||||
#user_table h4 { margin: 1em 0 0 0; }
|
||||
#user_table tr.account_inactive td.address { color: #888; text-decoration: line-through; }
|
||||
#user_table .actions { margin-top: .33em; font-size: 95%; }
|
||||
#user_table .account_inactive .if_active { display: none; }
|
||||
@@ -36,7 +35,7 @@
|
||||
<button type="submit" class="btn btn-primary">Add User</button>
|
||||
</form>
|
||||
<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>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>
|
||||
@@ -183,8 +182,8 @@ function show_users() {
|
||||
function(r) {
|
||||
$('#user_table tbody').html("");
|
||||
for (var i = 0; i < r.length; i++) {
|
||||
var hdr = $("<tr><td colspan='6'><h4/></td></tr>");
|
||||
hdr.find('h4').text(r[i].domain);
|
||||
var hdr = $("<tr><th colspan='6' style='background-color: #EEE'></th></tr>");
|
||||
hdr.find('th').text(r[i].domain);
|
||||
$('#user_table tbody').append(hdr);
|
||||
|
||||
for (var k = 0; k < r[i].users.length; k++) {
|
||||
|
||||
Reference in New Issue
Block a user