1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-14 17:27:23 +01:00

Merge upstream v0.51

This commit is contained in:
David Duque
2020-11-15 18:30:19 +00:00
22 changed files with 1414 additions and 683 deletions

View File

@@ -1,420 +1,411 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{hostname}} - Mail-in-a-Box Control Panel</title>
<title>{{hostname}} - Mail-in-a-Box Control Panel</title>
<meta name="robots" content="noindex, nofollow">
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
<style>
body {
overflow-y: scroll;
padding-bottom: 20px;
}
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
<style>
body {
overflow-y: scroll;
padding-bottom: 20px;
}
p {
margin-bottom: 1.25em;
}
p {
margin-bottom: 1.25em;
}
h1, h2, h3, h4 {
font-family: sans-serif;
font-weight: bold;
}
h1,
h2,
h3,
h4 {
font-family: sans-serif;
font-weight: bold;
}
h2 {
margin: 1em 0;
}
h2 {
margin: 1em 0;
}
h3 {
font-size: 130%;
border-bottom: 1px solid black;
padding-bottom: 3px;
margin-bottom: 13px;
margin-top: 30px;
}
.panel-heading h3 {
border: none;
padding: 0;
margin: 0;
}
h3 {
font-size: 130%;
border-bottom: 1px solid black;
padding-bottom: 3px;
margin-bottom: 13px;
margin-top: 30px;
}
h4 {
font-size: 110%;
margin-bottom: 13px;
margin-top: 18px;
}
h4:first-child {
margin-top: 6px;
}
.panel-heading h3 {
border: none;
padding: 0;
margin: 0;
}
.admin_panel {
display: none;
}
h4 {
font-size: 110%;
margin-bottom: 13px;
margin-top: 18px;
}
table.table {
margin: 1.5em 0;
}
h4:first-child {
margin-top: 6px;
}
ol li {
margin-bottom: 1em;
}
</style>
</head>
<body>
.admin_panel {
display: none;
}
<!--[if lt IE 8]><p>Internet Explorer version 8 or any modern web browser is required to use this website, sorry.<![endif]-->
<!--[if gt IE 7]><!-->
table.table {
margin: 1.5em 0;
}
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container bg-light">
<div class="navbar-header">
<a class="navbar-brand" href="#">{{hostname}}</a>
</div>
<div class="navbar navbar-expand-lg">
<ul class="nav navbar-nav">
<li class="btn dropdown">
<a style="color: black;" href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown-item"><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
<li class="dropdown-item"><a href="#tls" onclick="return show_panel(this);">TLS (SSL) Certificates</a></li>
<li class="dropdown-item"><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
<li class="dropdown-item"><a href="#smtp_relays" onclick="return show_panel(this);">SMTP Relays</a></li>
<li class="divider"></li>
<li class="dropdown-header">Advanced Pages</li>
<li class="dropdown-item"><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
<li class="dropdown-item"><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
<li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
</ul>
</li>
<li class="btn dropdown">
<a style="color: black;" href="#" class="dropdown-toggle" data-toggle="dropdown">Mail &amp; Users <b class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown-item"><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></li>
<li class="dropdown-item"><a href="#users" onclick="return show_panel(this);">Users</a></li>
<li class="dropdown-item"><a href="#aliases" onclick="return show_panel(this);">Aliases</a></li>
<li class="divider"></li>
<li class="dropdown-header">Your Account</li>
<li class="dropdown-item"><a href="#mfa" onclick="return show_panel(this);">Two-Factor Authentication</a></li>
</ul>
</li>
<li class="btn"><a style="color: black;" href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li class="btn"><a style="color: black;" href="#web" onclick="return show_panel(this);">Web</a></li>
</ul>
<ul class="btn nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: black; font-weight: bold;">Log out</a></li>
</ul>
</div><!--/.navbar-collapse -->
</div>
</div>
ol li {
margin-bottom: 1em;
}
</style>
</head>
<div class="container">
<div id="panel_smtp_relays" class="admin_panel">
{% include "smtp-relays.html" %}
</div>
<body>
<div id="panel_system_status" class="admin_panel">
{% include "system-status.html" %}
</div>
<!--[if lt IE 8]><p>Internet Explorer version 8 or any modern web browser is required to use this website, sorry.<![endif]-->
<!--[if gt IE 7]><!-->
<div id="panel_system_backup" class="admin_panel">
{% include "system-backup.html" %}
</div>
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container bg-light">
<div class="navbar-header">
<a class="navbar-brand" href="#">{{hostname}}</a>
</div>
<div class="navbar navbar-expand-lg">
<ul class="nav navbar-nav">
<li class="btn dropdown">
<a style="color: black;" href="#" class="dropdown-toggle" data-toggle="dropdown">System <b
class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown-item"><a href="#system_status" onclick="return show_panel(this);">Status
Checks</a></li>
<li class="dropdown-item"><a href="#tls" onclick="return show_panel(this);">TLS (SSL)
Certificates</a></li>
<li class="dropdown-item"><a href="#system_backup" onclick="return show_panel(this);">Backup
Status</a></li>
<li class="dropdown-item"><a href="#smtp_relays" onclick="return show_panel(this);">SMTP
Relays</a></li>
<li class="divider"></li>
<li class="dropdown-header">Advanced Pages</li>
<li class="dropdown-item"><a href="#custom_dns" onclick="return show_panel(this);">Custom
DNS</a></li>
<li class="dropdown-item"><a href="#external_dns"
onclick="return show_panel(this);">External DNS</a></li>
<li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
</ul>
</li>
<li class="btn dropdown">
<a style="color: black;" href="#" class="dropdown-toggle" data-toggle="dropdown">Mail <b
class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown-item"><a href="#mail-guide"
onclick="return show_panel(this);">Instructions</a></li>
<li class="dropdown-item"><a href="#users" onclick="return show_panel(this);">Users</a></li>
<li class="dropdown-item"><a href="#aliases" onclick="return show_panel(this);">Aliases</a>
</li>
</ul>
</li>
<li class="btn"><a style="color: black;" href="#sync_guide"
onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li class="btn"><a style="color: black;" href="#web" onclick="return show_panel(this);">Web</a></li>
</ul>
<ul class="btn nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: black; font-weight: bold;">Log
out</a></li>
</ul>
</div>
<!--/.navbar-collapse -->
</div>
</div>
<div id="panel_external_dns" class="admin_panel">
{% include "external-dns.html" %}
</div>
<div class="container">
<div id="panel_smtp_relays" class="admin_panel">
{% include "smtp-relays.html" %}
</div>
<div id="panel_custom_dns" class="admin_panel">
{% include "custom-dns.html" %}
</div>
<div id="panel_system_status" class="admin_panel">
{% include "system-status.html" %}
</div>
<div id="panel_mfa" class="admin_panel">
{% include "mfa.html" %}
</div>
<div id="panel_system_backup" class="admin_panel">
{% include "system-backup.html" %}
</div>
<div id="panel_login" class="admin_panel">
{% include "login.html" %}
</div>
<div id="panel_external_dns" class="admin_panel">
{% include "external-dns.html" %}
</div>
<div id="panel_mail-guide" class="admin_panel">
{% include "mail-guide.html" %}
</div>
<div id="panel_custom_dns" class="admin_panel">
{% include "custom-dns.html" %}
</div>
<div id="panel_users" class="admin_panel">
{% include "users.html" %}
</div>
<div id="panel_login" class="admin_panel">
{% include "login.html" %}
</div>
<div id="panel_aliases" class="admin_panel">
{% include "aliases.html" %}
</div>
<div id="panel_mail-guide" class="admin_panel">
{% include "mail-guide.html" %}
</div>
<div id="panel_sync_guide" class="admin_panel">
{% include "sync-guide.html" %}
</div>
<div id="panel_users" class="admin_panel">
{% include "users.html" %}
</div>
<div id="panel_web" class="admin_panel">
{% include "web.html" %}
</div>
<div id="panel_aliases" class="admin_panel">
{% include "aliases.html" %}
</div>
<div id="panel_tls" class="admin_panel">
{% include "ssl.html" %}
</div>
<div id="panel_sync_guide" class="admin_panel">
{% include "sync-guide.html" %}
</div>
<hr>
<div id="panel_web" class="admin_panel">
{% include "web.html" %}
</div>
<footer>
<p>This is a <a href="https://github.com/ddavness/power-mailinabox">Power Mail-in-a-Box</a> - {{distname}}</p>
</footer>
</div> <!-- /container -->
<div id="panel_tls" class="admin_panel">
{% include "ssl.html" %}
</div>
<div id="ajax_loading_indicator" style="display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 100000; text-align: center; background-color: rgba(255,255,255,.75)">
<div style="margin: 20% auto">
<div><span class="fa fa-spinner fa-pulse"></span></div>
<div>Loading...</div>
</div>
</div>
<hr>
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="errorModalTitle"> </h4>
</div>
<div class="modal-body">
<p> </p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">Yes</button>
</div>
</div>
</div>
</div>
<footer>
<p>This is a <a href="https://github.com/ddavness/power-mailinabox">Power Mail-in-a-Box</a> - {{distname}}
</p>
</footer>
</div> <!-- /container -->
<script src="/admin/assets/jquery.min.js"></script>
<script src="/admin/assets/bootstrap/js/bootstrap.min.js"></script>
<div id="ajax_loading_indicator"
style="display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 100000; text-align: center; background-color: rgba(255,255,255,.75)">
<div style="margin: 20% auto">
<div><span class="fa fa-spinner fa-pulse"></span></div>
<div>Loading...</div>
</div>
</div>
<script>
var global_modal_state = null;
var global_modal_funcs = null;
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle"
aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="errorModalTitle"> </h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<p> </p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">Yes</button>
</div>
</div>
</div>
</div>
$(function() {
$('#global_modal').on('shown.bs.modal', function (e) {
// set focus to first input in the global modal's body
var input = $('#global_modal .modal-body input');
if (input.length > 0) $(input[0]).focus();
})
$('#global_modal .btn-danger').click(function() {
// Don't take action now. Wait for the modal to be totally hidden
// so that we don't attempt to show another modal while this one
// is closing.
global_modal_state = 0; // OK
})
$('#global_modal .btn-default').click(function() {
global_modal_state = 1; // Cancel
})
$('#global_modal').on('hidden.bs.modal', function (e) {
// do the cancel function
if (global_modal_state == null) global_modal_state = 1; // cancel if the user hit ESC or clicked outside of the modal
if (global_modal_funcs && global_modal_funcs[global_modal_state])
global_modal_funcs[global_modal_state]();
})
})
<script src="/admin/assets/jquery.min.js"></script>
<script src="/admin/assets/bootstrap/js/bootstrap.min.js"></script>
function show_modal_error(title, message, callback) {
$('#global_modal h4').text(title);
$('#global_modal .modal-body').html("<p/>");
if (typeof question == 'string') {
$('#global_modal p').text(message);
$('#global_modal .modal-dialog').addClass("modal-sm");
} else {
$('#global_modal p').html("").append(message);
$('#global_modal .modal-dialog').removeClass("modal-sm");
}
$('#global_modal .btn-default').show().text("OK");
$('#global_modal .btn-danger').hide();
global_modal_funcs = [callback, callback];
global_modal_state = null;
$('#global_modal').modal({});
return false; // handy when called from onclick
}
<script>
var global_modal_state = null;
var global_modal_funcs = null;
function show_modal_confirm(title, question, verb, yes_callback, cancel_callback) {
$('#global_modal h4').text(title);
if (typeof question == 'string') {
$('#global_modal .modal-dialog').addClass("modal-sm");
$('#global_modal .modal-body').html("<p/>");
$('#global_modal p').text(question);
} else {
$('#global_modal .modal-dialog').removeClass("modal-sm");
$('#global_modal .modal-body').html("").append(question);
}
if (typeof verb == 'string') {
$('#global_modal .btn-default').show().text("Cancel");
$('#global_modal .btn-danger').show().text(verb);
} else {
$('#global_modal .btn-default').show().text(verb[1]);
$('#global_modal .btn-danger').show().text(verb[0]);
}
global_modal_funcs = [yes_callback, cancel_callback];
global_modal_state = null;
$('#global_modal').modal({});
return false; // handy when called from onclick
}
$(function () {
$('#global_modal').on('shown.bs.modal', function (e) {
// set focus to first input in the global modal's body
var input = $('#global_modal .modal-body input');
if (input.length > 0) $(input[0]).focus();
})
$('#global_modal .btn-danger').click(function () {
// Don't take action now. Wait for the modal to be totally hidden
// so that we don't attempt to show another modal while this one
// is closing.
global_modal_state = 0; // OK
})
$('#global_modal .btn-default').click(function () {
global_modal_state = 1; // Cancel
})
$('#global_modal').on('hidden.bs.modal', function (e) {
// do the cancel function
if (global_modal_state == null) global_modal_state = 1; // cancel if the user hit ESC or clicked outside of the modal
if (global_modal_funcs && global_modal_funcs[global_modal_state])
global_modal_funcs[global_modal_state]();
})
})
var ajax_num_executing_requests = 0;
function ajax_with_indicator(options) {
setTimeout("if (ajax_num_executing_requests > 0) $('#ajax_loading_indicator').fadeIn()", 100);
function hide_loading_indicator() {
ajax_num_executing_requests--;
if (ajax_num_executing_requests == 0)
$('#ajax_loading_indicator').stop(true).hide(); // stop() prevents an ongoing fade from causing the thing to be shown again after this call
}
var old_success = options.success;
var old_error = options.error;
options.success = function(data) {
hide_loading_indicator();
if (data.status == "error")
show_modal_error("Error", data.message);
else if (old_success)
old_success(data);
};
options.error = function(jqxhr) {
hide_loading_indicator();
if (!old_error)
show_modal_error("Error", "Something went wrong, sorry.")
else
old_error(jqxhr.responseText, jqxhr);
};
ajax_num_executing_requests++;
$.ajax(options);
return false; // handy when called from onclick
}
function show_modal_error(title, message, callback) {
$('#global_modal h4').text(title);
$('#global_modal .modal-body').html("<p/>");
if (typeof question == 'string') {
$('#global_modal p').text(message);
$('#global_modal .modal-dialog').addClass("modal-sm");
} else {
$('#global_modal p').html("").append(message);
$('#global_modal .modal-dialog').removeClass("modal-sm");
}
$('#global_modal .btn-default').show().text("OK");
$('#global_modal .btn-danger').hide();
global_modal_funcs = [callback, callback];
global_modal_state = null;
$('#global_modal').modal({});
return false; // handy when called from onclick
}
var api_credentials = ["", ""];
function api(url, method, data, callback, callback_error, headers) {
// from http://www.webtoolkit.info/javascript-base64.html
function base64encode(input) {
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
function show_modal_confirm(title, question, verb, yes_callback, cancel_callback) {
$('#global_modal h4').text(title);
if (typeof question == 'string') {
$('#global_modal .modal-dialog').addClass("modal-sm");
$('#global_modal .modal-body').html("<p/>");
$('#global_modal p').text(question);
} else {
$('#global_modal .modal-dialog').removeClass("modal-sm");
$('#global_modal .modal-body').html("").append(question);
}
if (typeof verb == 'string') {
$('#global_modal .btn-default').show().text("Cancel");
$('#global_modal .btn-danger').show().text(verb);
} else {
$('#global_modal .btn-default').show().text(verb[1]);
$('#global_modal .btn-danger').show().text(verb[0]);
}
global_modal_funcs = [yes_callback, cancel_callback];
global_modal_state = null;
$('#global_modal').modal({});
return false; // handy when called from onclick
}
return output;
}
var ajax_num_executing_requests = 0;
function ajax_with_indicator(options) {
setTimeout("if (ajax_num_executing_requests > 0) $('#ajax_loading_indicator').fadeIn()", 100);
function hide_loading_indicator() {
ajax_num_executing_requests--;
if (ajax_num_executing_requests == 0)
$('#ajax_loading_indicator').stop(true).hide(); // stop() prevents an ongoing fade from causing the thing to be shown again after this call
}
var old_success = options.success;
var old_error = options.error;
options.success = function (data) {
hide_loading_indicator();
if (data.status == "error")
show_modal_error("Error", data.message);
else if (old_success)
old_success(data);
};
options.error = function (jqxhr) {
hide_loading_indicator();
if (!old_error)
show_modal_error("Error", "Something went wrong, sorry.")
else
old_error(jqxhr.responseText, jqxhr);
};
ajax_num_executing_requests++;
$.ajax(options);
return false; // handy when called from onclick
}
function default_error(text, xhr) {
if (xhr.status != 403) // else handled below
show_modal_error("Error", "Something went wrong, sorry.")
}
var api_credentials = ["", ""];
function api(url, method, data, callback, callback_error) {
// from http://www.webtoolkit.info/javascript-base64.html
function base64encode(input) {
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
ajax_with_indicator({
url: "/admin" + url,
method: method,
cache: false,
data: data,
headers: headers,
// the custom DNS api sends raw POST/PUT bodies --- prevent URL-encoding
processData: typeof data != "string",
mimeType: typeof data == "string" ? "text/plain; charset=ascii" : null,
return output;
}
beforeSend: function(xhr) {
// We don't store user credentials in a cookie to avoid the hassle of CSRF
// attacks. The Authorization header only gets set in our AJAX calls triggered
// by user actions.
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials[0] + ':' + api_credentials[1]));
},
success: callback,
error: callback_error || default_error,
statusCode: {
403: function(xhr) {
// Credentials are no longer valid. Try to login again.
var p = current_panel;
show_panel('login');
switch_back_to_panel = p;
}
}
})
}
function default_error(_, xhr) {
if (xhr.status != 403) // else handled below
show_modal_error("Error", "Something went wrong, sorry.")
}
var current_panel = null;
var switch_back_to_panel = null;
ajax_with_indicator({
url: "/admin" + url,
method: method,
cache: false,
data: data,
function do_logout() {
api_credentials = ["", ""];
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
show_panel('login');
}
// the custom DNS api sends raw POST/PUT bodies --- prevent URL-encoding
processData: typeof data != "string",
mimeType: typeof data == "string" ? "text/plain; charset=ascii" : null,
function show_panel(panelid) {
if (panelid.getAttribute)
// we might be passed an HTMLElement <a>.
panelid = panelid.getAttribute('href').substring(1);
beforeSend: function (xhr) {
// We don't store user credentials in a cookie to avoid the hassle of CSRF
// attacks. The Authorization header only gets set in our AJAX calls triggered
// by user actions.
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials[0] + ':' + api_credentials[1]));
},
success: callback,
error: callback_error || default_error,
statusCode: {
403: function (xhr) {
// Credentials are no longer valid. Try to login again.
var p = current_panel;
show_panel('login');
// No use of going back
switch_back_to_panel = current_panel;
}
}
})
}
$('.admin_panel').hide();
$('#panel_' + panelid).show();
if (typeof localStorage != 'undefined')
localStorage.setItem("miab-cp-lastpanel", panelid);
if (window["show_" + panelid])
window["show_" + panelid]();
var current_panel = null;
var switch_back_to_panel = null;
function show_panel(panelid) {
if (panelid.getAttribute) {
// we might be passed an HTMLElement <a>.
panelid = panelid.getAttribute('href').substring(1);
}
current_panel = panelid;
switch_back_to_panel = null;
$('.admin_panel').hide();
$('#panel_' + panelid).show();
if (typeof localStorage != 'undefined')
localStorage.setItem("miab-cp-lastpanel", panelid);
if (window["show_" + panelid])
window["show_" + panelid]();
return false; // when called from onclick, cancel navigation
}
current_panel = panelid;
switch_back_to_panel = null;
$(function() {
// Recall saved user credentials.
if (typeof sessionStorage != 'undefined' && sessionStorage.getItem("miab-cp-credentials"))
api_credentials = sessionStorage.getItem("miab-cp-credentials").split(":");
else if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-credentials"))
api_credentials = localStorage.getItem("miab-cp-credentials").split(":");
return false; // when called from onclick, cancel navigation
}
$(function () {
// Recall saved user credentials.
if (typeof sessionStorage != 'undefined' && sessionStorage.getItem("miab-cp-credentials"))
api_credentials = sessionStorage.getItem("miab-cp-credentials").split(":");
else if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-credentials"))
api_credentials = localStorage.getItem("miab-cp-credentials").split(":");
// Recall what the user was last looking at.
if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
show_panel(localStorage.getItem("miab-cp-lastpanel"));
} else {
show_panel('login');
}
})
</script>
</body>
// Recall what the user was last looking at.
if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
show_panel(localStorage.getItem("miab-cp-lastpanel"));
} else {
show_panel('login');
}
})
</script>
</body>
</html>

View File

@@ -1,143 +1,186 @@
<h1 style="margin: 1em; text-align: center">{{hostname}}</h1>
<style>
.title {
margin: 1em;
text-align: center;
}
.subtitle {
margin: 2em;
text-align: center;
}
.login {
margin: 0 auto;
max-width: 32em;
}
.login #loginOtp {
display: none;
}
#loginForm.is-twofactor #loginOtp {
display: block
}
</style>
<h1 class="title">{{hostname}}</h1>
{% if no_users_exist or no_admins_exist %}
<div class="row">
<div class="col-md-offset-2 col-md-8">
{% if no_users_exist %}
<p class="text-danger">There are no users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo tools/mail.py user add me@{{hostname}}
sudo tools/mail.py user make-admin me@{{hostname}}</pre>
{% else %}
<p class="text-danger">There are no administrative users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo tools/mail.py user make-admin me@{{hostname}}</pre>
{% endif %}
<hr>
</div>
<div class="col-md-offset-2 col-md-8">
{% if no_users_exist %}
<p class="text-danger">There are no users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo management/cli.py user add me@{{hostname}}
sudo management/cli.py user make-admin me@{{hostname}}</pre>
{% else %}
<p class="text-danger">There are no administrative users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo management/cli.py user make-admin me@{{hostname}}</pre>
{% endif %}
<hr>
</div>
</div>
{% endif %}
<p style="margin: 2em; text-align: center;">Log in here for your Mail-in-a-Box control panel.</p>
<p class="subtitle">Log in here for your Mail-in-a-Box control panel.</p>
<div style="margin: 0 auto; max-width: 32em;">
<form class="form-horizontal" role="form" onsubmit="do_login(); return false;" method="get">
<div class="form-group">
<label for="inputEmail3" class="col-sm-3 control-label">Email</label>
<div class="col-sm-12">
<input name="email" type="email" class="form-control" id="loginEmail" placeholder="Email">
</div>
</div>
<div class="form-group">
<label for="inputPassword3" class="col-sm-3 control-label">Password</label>
<div class="col-sm-12">
<input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="checkbox">
<label>
<input name='remember' type="checkbox" id="loginRemember"> Remember me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
<div class="login">
<form id="loginForm" class="form-horizontal" role="form" onsubmit="do_login(); return false;" method="get">
<div class="form-group">
<label for="inputEmail3" class="col-sm-3 control-label">Email</label>
<div class="col-sm-12">
<input name="email" type="email" class="form-control" id="loginEmail" placeholder="Email">
</div>
</div>
<div class="form-group">
<label for="inputPassword3" class="col-sm-3 control-label">Password</label>
<div class="col-sm-12">
<input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password">
</div>
</div>
<div class="form-group" id="loginOtp">
<label for="loginOtpInput" class="col-sm-3 control-label">Code</label>
<div class="col-sm-12">
<input type="text" class="form-control" id="loginOtpInput" placeholder="6-digit code">
<div class="help-block" style="margin-top: 5px; font-size: 90%">Enter the six-digit code generated by your two factor authentication app.</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="checkbox">
<label>
<input name='remember' type="checkbox" id="loginRemember"> Remember me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
</div>
<script>
function do_login() {
if ($('#loginEmail').val() == "") {
show_modal_error("Login Failed", "Enter your email address.", function () {
$('#loginEmail').focus();
});
return false;
}
if ($('#loginPassword').val() == "") {
show_modal_error("Login Failed", "Enter your email password.", function () {
$('#loginPassword').focus();
});
return false;
}
function do_login() {
if ($('#loginEmail').val() == "") {
show_modal_error("Login Failed", "Enter your email address.", function() {
$('#loginEmail').focus();
});
return false;
}
// Exchange the email address & password for an API key.
api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()]
if ($('#loginPassword').val() == "") {
show_modal_error("Login Failed", "Enter your email password.", function() {
$('#loginPassword').focus();
});
return false;
}
api(
"/me",
"GET",
{},
function (response) {
// This API call always succeeds. It returns a JSON object indicating
// whether the request was authenticated or not.
if (response.status != "ok") {
// Show why the login failed.
show_modal_error("Login Failed", response.reason)
// Exchange the email address & password for an API key.
api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()]
// Reset any saved credentials.
do_logout();
api(
"/me",
"GET",
{},
function(response) {
// This API call always succeeds. It returns a JSON object indicating
// whether the request was authenticated or not.
if (response.status != 'ok') {
if (response.status === 'missing-totp-token' || (response.status === 'invalid' && response.reason == 'invalid-totp-token')) {
$('#loginForm').addClass('is-twofactor');
if (response.reason === "invalid-totp-token") {
show_modal_error("Login Failed", "Incorrect two factor authentication token.");
} else {
setTimeout(() => {
$('#loginOtpInput').focus();
});
}
} else {
$('#loginForm').removeClass('is-twofactor');
} else if (!("api_key" in response)) {
// Login succeeded but user might not be authorized!
show_modal_error("Login Failed", "You are not an administrator on this system.")
// Show why the login failed.
show_modal_error("Login Failed", response.reason)
// Reset any saved credentials.
do_logout();
// Reset any saved credentials.
do_logout();
}
} else if (!("api_key" in response)) {
// Login succeeded but user might not be authorized!
show_modal_error("Login Failed", "You are not an administrator on this system.")
} else {
// Login succeeded.
// Reset any saved credentials.
do_logout();
// Save the new credentials.
api_credentials = [response.email, response.api_key];
} else {
// Login succeeded.
// Try to wipe the username/password information.
$('#loginEmail').val('');
$('#loginPassword').val('');
// Save the new credentials.
api_credentials = [response.email, response.api_key];
// Remember the credentials.
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
if ($('#loginRemember').val()) {
localStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
sessionStorage.removeItem("miab-cp-credentials");
} else {
localStorage.removeItem("miab-cp-credentials");
sessionStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
}
}
// Try to wipe the username/password information.
$('#loginEmail').val('');
$('#loginPassword').val('');
$('#loginOtpInput').val('');
$('#loginForm').removeClass('is-twofactor');
// Open the next panel the user wants to go to. Do this after the XHR response
// is over so that we don't start a new XHR request while this one is finishing,
// which confuses the loading indicator.
setTimeout(function () { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
}
})
}
// Remember the credentials.
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
if ($('#loginRemember').val()) {
localStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
sessionStorage.removeItem("miab-cp-credentials");
} else {
localStorage.removeItem("miab-cp-credentials");
sessionStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
}
}
function do_logout() {
api_credentials = ["", ""];
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
show_panel('login');
}
// Open the next panel the user wants to go to. Do this after the XHR response
// is over so that we don't start a new XHR request while this one is finishing,
// which confuses the loading indicator.
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
}
},
undefined,
{
'x-auth-token': $('#loginOtpInput').val()
});
}
function show_login() {
$('#loginEmail,#loginPassword').each(function () {
var input = $(this);
if (!$.trim(input.val())) {
input.focus();
return false;
}
});
}
function show_login() {
$('#loginForm').removeClass('is-twofactor');
$('#loginOtpInput').val('');
$('#loginEmail,#loginPassword').each(function() {
var input = $(this);
if (!$.trim(input.val())) {
input.focus();
return false;
}
});
}
</script>

View File

@@ -0,0 +1,242 @@
<style>
.twofactor #totp-setup,
.twofactor #disable-2fa,
.twofactor #output-2fa {
display: none;
}
.twofactor.loaded .loading-indicator {
display: none;
}
.twofactor.disabled #disable-2fa,
.twofactor.enabled #totp-setup {
display: none;
}
.twofactor.disabled #totp-setup,
.twofactor.enabled #disable-2fa {
display: block;
}
.twofactor #totp-setup-qr img {
display: block;
width: 256px;
max-width: 100%;
height: auto;
}
.twofactor #output-2fa.visible {
display: block;
}
</style>
<h2>Two-Factor Authentication</h2>
<p>When two-factor authentication is enabled, you will be prompted to enter a six digit code from an
authenticator app (usually on your phone) when you log into this control panel.</p>
<div class="card">
<div class="card-header text-white bg-danger">
Enabling two-factor authentication does not protect access to your email
</div>
<div class="card-body bg-light">
Enabling two-factor authentication on this page only limits access to this control panel. Remember that most websites allow you to
reset your password by checking your email, so anyone with access to your email can typically take over
your other accounts. Additionally, if your email address or any alias that forwards to your email
address is a typical domain control validation address (e.g admin@, administrator@, postmaster@, hostmaster@,
webmaster@, abuse@), extra care should be taken to protect the account. <strong>Always use a strong password,
and ensure every administrator account for this control panel does the same.</strong>
</div>
</div>
<div class="twofactor">
<div class="loading-indicator">Loading...</div>
<form id="totp-setup">
<h3>Setup Instructions</h3>
<div class="form-group">
<p>1. Install <a href="https://freeotp.github.io/">FreeOTP</a> or <a href="https://www.pcworld.com/article/3225913/what-is-two-factor-authentication-and-which-2fa-apps-are-best.html">any
other two-factor authentication app</a> that supports TOTP.</p>
</div>
<div class="form-group">
<p style="margin-bottom: 0">2. Scan the QR code in the app or directly enter the secret into the app:</p>
<div id="totp-setup-qr"></div>
</div>
<div class="form-group">
<label for="otp-label" style="font-weight: normal">3. Optionally, give your device a label so that you can remember what device you set it up on:</label>
<input type="text" id="totp-setup-label" class="form-control" placeholder="my phone" />
</div>
<div class="form-group">
<label for="otp" style="font-weight: normal">4. Use the app to generate your first six-digit code and enter it here:</label>
<input type="text" id="totp-setup-token" class="form-control" placeholder="6-digit code" />
</div>
<input type="hidden" id="totp-setup-secret" />
<div class="form-group">
<p>When you click Enable Two-Factor Authentication, you will be logged out of the control panel and will have to log in
again, now using your two-factor authentication app.</p>
<button id="totp-setup-submit" disabled type="submit" class="btn btn-primary">Enable Two-Factor Authentication</button>
</div>
</form>
<form id="disable-2fa">
<div class="form-group">
<p>Two-factor authentication is active for your account<span id="mfa-device-label"></span>.</p>
<p>You will have to log into the admin panel again after disabling two-factor authentication.</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-danger">Disable Two-Factor Authentication</button>
</div>
</form>
<div id="output-2fa" class="card bg-light">
<div class="card-body"></div>
</div>
</div>
<script>
var el = {
disableForm: document.getElementById('disable-2fa'),
output: document.getElementById('output-2fa'),
totpSetupForm: document.getElementById('totp-setup'),
totpSetupToken: document.getElementById('totp-setup-token'),
totpSetupSecret: document.getElementById('totp-setup-secret'),
totpSetupLabel: document.getElementById('totp-setup-label'),
totpQr: document.getElementById('totp-setup-qr'),
totpSetupSubmit: document.querySelector('#totp-setup-submit'),
wrapper: document.querySelector('.twofactor')
}
function update_setup_disabled(evt) {
var val = evt.target.value.trim();
if (
typeof val !== 'string' ||
typeof el.totpSetupSecret.value !== 'string' ||
val.length !== 6 ||
el.totpSetupSecret.value.length !== 32 ||
!(/^\+?\d+$/.test(val))
) {
el.totpSetupSubmit.setAttribute('disabled', '');
} else {
el.totpSetupSubmit.removeAttribute('disabled');
}
}
function render_totp_setup(provisioned_totp) {
var img = document.createElement('img');
img.src = "data:image/png;base64," + provisioned_totp.qr_code_base64;
var code = document.createElement('div');
code.innerHTML = `Secret: ${provisioned_totp.secret}`;
el.totpQr.appendChild(img);
el.totpQr.appendChild(code);
el.totpSetupToken.addEventListener('input', update_setup_disabled);
el.totpSetupForm.addEventListener('submit', do_enable_totp);
el.totpSetupSecret.setAttribute('value', provisioned_totp.secret);
el.wrapper.classList.add('disabled');
}
function render_disable(mfa) {
el.disableForm.addEventListener('submit', do_disable);
el.wrapper.classList.add('enabled');
if (mfa.label)
$("#mfa-device-label").text(" on device '" + mfa.label + "'");
}
function hide_error() {
el.output.querySelector('.card-body').innerHTML = '';
el.output.classList.remove('visible');
}
function render_error(msg) {
el.output.querySelector('.card-body').innerHTML = msg;
el.output.classList.add('visible');
}
function reset_view() {
el.wrapper.classList.remove('loaded', 'disabled', 'enabled');
el.disableForm.removeEventListener('submit', do_disable);
hide_error();
el.totpSetupForm.reset();
el.totpSetupForm.removeEventListener('submit', do_enable_totp);
el.totpSetupSecret.setAttribute('value', '');
el.totpSetupToken.removeEventListener('input', update_setup_disabled);
el.totpSetupSubmit.setAttribute('disabled', '');
el.totpQr.innerHTML = '';
}
function show_mfa() {
reset_view();
api(
'/mfa/status',
'POST',
{},
function(res) {
el.wrapper.classList.add('loaded');
var has_mfa = false;
res.enabled_mfa.forEach(function(mfa) {
if (mfa.type == "totp") {
render_disable(mfa);
has_mfa = true;
}
});
if (!has_mfa)
render_totp_setup(res.new_mfa.totp);
}
);
}
function do_disable(evt) {
evt.preventDefault();
hide_error();
api(
'/mfa/disable',
'POST',
{ type: 'totp' },
function() {
do_logout();
}
);
return false;
}
function do_enable_totp(evt) {
evt.preventDefault();
hide_error();
api(
'/mfa/totp/enable',
'POST',
{
token: $(el.totpSetupToken).val(),
secret: $(el.totpSetupSecret).val(),
label: $(el.totpSetupLabel).val()
},
function(res) { do_logout(); },
function(res) { render_error(res); }
);
return false;
}
</script>

View File

@@ -113,10 +113,10 @@
</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">Days:</label>
<label for="min-age" class="col-sm-2 control-label">Retention Days:</label>
<div class="col-sm-8">
<input type="number" class="form-control" rows="1" id="min-age">
<div class="small" style="margin-top: 2px">This is the <i>minimum</i> number of days backup data is kept for. The box makes an incremental backup, so backup data is often kept much longer. An incremental backup file that is less than this number of days old requires that all previous increments back to the most recent full backup, plus that full backup, remain available.</div>
<div class="small" style="margin-top: 2px">This is the minimum time backup data is kept for. The box makes an incremental backup most nights, which requires that previous backups back to the most recent full backup be preserved, so backup data is often kept much longer than this setting. Full backups are made periodically when the incremental backup data size exceeds a limit.</div>
</div>
</div>
<div class="form-group">