Fix various Bootstrap container/row/column abuses resulting in broken layouts

Every column has to be in a row, and every row has to be in a container (except
where a row is nested directly within a column).

This also includes a change to index.html that pushes the footer to the bottom
of the page on those where it otherwise wouldn't be.
This commit is contained in:
David Piggott 2014-10-19 22:40:22 +01:00
parent 6585384daa
commit 3e261dee66
12 changed files with 1300 additions and 1172 deletions

View File

@ -1,198 +1,201 @@
<style> <div class="container-fluid">
#alias_table .actions > * { padding-right: 3px; }
#alias_table .alias-required .remove { display: none }
</style>
<h2>Aliases</h2> <style>
#alias_table .actions > * { padding-right: 3px; }
#alias_table .alias-required .remove { display: none }
</style>
<h3>Add a mail alias</h3> <div class="row">
<div class="col-sm-12">
<p>Aliases are email forwarders. An alias can forward email to a <a href="javascript:show_panel('users')">mail user</a> or to any email address.</p> <h2>Aliases</h2>
<form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;"> <h3>Add a mail alias</h3>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11"> <p>Aliases are email forwarders. An alias can forward email to a <a href="javascript:show_panel('users')">mail user</a> or to any email address.</p>
<div id="alias_type_buttons" class="btn-group btn-group-xs">
<button type="button" class="btn btn-default active">Regular</button> <form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
<button type="button" class="btn btn-default">Catch-All</button> <div class="form-group">
</div> <div class="col-sm-offset-1 col-sm-11">
<div id="alias_catchall_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">A catch-all alias captures all otherwise unmatched email to a domain. Enter just a part of an email address starting with the @-sign.</div> <div id="alias_type_buttons" class="btn-group btn-group-xs">
<button type="button" class="btn btn-default active">Regular</button>
<button type="button" class="btn btn-default">Catch-All</button>
</div>
<div id="alias_catchall_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">A catch-all alias captures all otherwise unmatched email to a domain. Enter just a part of an email address starting with the @-sign.</div>
</div>
</div>
<div class="form-group">
<label for="addaliasEmail" class="col-sm-1 control-label">Alias</label>
<div class="col-sm-10">
<input type="email" class="form-control" id="addaliasEmail" placeholder="incoming email address (you@yourdomain.com)">
</div>
</div>
<div class="form-group">
<label for="addaliasTargets" class="col-sm-1 control-label">Forward To</label>
<div class="col-sm-10">
<textarea class="form-control" rows="3" id="addaliasTargets" placeholder="forward to these email addresses (one per line or separated by commas)"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<button id="add-alias-button" type="submit" class="btn btn-primary">Add Alias</button>
<button id="alias-cancel" class="btn btn-default hidden" onclick="aliases_reset_form(); return false;">Cancel</button>
</div>
</div>
</form>
<h3>Existing mail aliases</h3>
<table id="alias_table" class="table" style="width: auto">
<thead>
<tr>
<th></th>
<th>Alias<br></th>
<th>Forwards To</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p style="margin-top: 1.5em"><small>Hostmaster@, postmaster@ and admin@ email addresses are required on some domains.</small></p>
<div style="display: none">
<table>
<tr id="alias-template">
<td class='actions'>
<a href="#" onclick="aliases_edit(this); scroll_top(); return false;" class='edit' title="Edit Alias">
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="#" onclick="aliases_remove(this); return false;" class='remove' title="Remove Alias">
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
<td class='email'> </td>
<td class='target'> </td>
</tr>
</table>
</div>
</div>
</div> </div>
</div>
<div class="form-group">
<label for="addaliasEmail" class="col-sm-1 control-label">Alias</label>
<div class="col-sm-10">
<input type="email" class="form-control" id="addaliasEmail" placeholder="incoming email address (you@yourdomain.com)">
</div>
</div>
<div class="form-group">
<label for="addaliasTargets" class="col-sm-1 control-label">Forward To</label>
<div class="col-sm-10">
<textarea class="form-control" rows="3" id="addaliasTargets" placeholder="forward to these email addresses (one per line or separated by commas)"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<button id="add-alias-button" type="submit" class="btn btn-primary">Add Alias</button>
<button id="alias-cancel" class="btn btn-default hidden" onclick="aliases_reset_form(); return false;">Cancel</button>
</div>
</div>
</form>
<h3>Existing mail aliases</h3> <script>
<table id="alias_table" class="table" style="width: auto"> function show_aliases() {
<thead> $('#alias_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
<tr>
<th></th>
<th>Alias<br></th>
<th>Forwards To</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p style="margin-top: 1.5em"><small>Hostmaster@, postmaster@, and admin@ email addresses are required on some domains.</small></p>
<div style="display: none">
<table>
<tr id="alias-template">
<td class='actions'>
<a href="#" onclick="aliases_edit(this); scroll_top(); return false;" class='edit' title="Edit Alias">
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="#" onclick="aliases_remove(this); return false;" class='remove' title="Remove Alias">
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
<td class='email'> </td>
<td class='target'> </td>
</tr>
</table>
</div>
<script>
function show_aliases() {
$('#alias_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/mail/aliases",
"GET",
{ format: 'json' },
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);
$('#alias_table tbody').append(hdr);
for (var k = 0; k < r[i].aliases.length; k++) {
var alias = r[i].aliases[k];
var n = $("#alias-template").clone();
n.attr('id', '');
if (alias.required) n.addClass('alias-required');
n.attr('data-email', alias.source);
n.find('td.email').text(alias.source)
for (var j = 0; j < alias.destination.length; j++)
n.find('td.target').append($("<div></div>").text(alias.destination[j]))
$('#alias_table tbody').append(n);
}
}
})
$(function() {
$('#alias_type_buttons button').off('click').click(function() {
$('#alias_type_buttons button').removeClass('active');
$(this).addClass('active');
if ($(this).text() == "Regular") {
$('#addaliasEmail').attr('type', 'email');
$('#addaliasEmail').attr('placeholder', 'incoming email address (you@yourdomain.com)');
$('#alias_catchall_info').slideUp();
} else {
$('#addaliasEmail').attr('type', 'text');
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (@yourdomain.com)');
$('#alias_catchall_info').slideDown();
}
})
})
}
var is_alias_add_update = false;
function do_add_alias() {
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
var email = $("#addaliasEmail").val();
var targets = $("#addaliasTargets").val();
api(
"/mail/aliases/add",
"POST",
{
update_if_exists: is_alias_add_update ? '1' : '0',
source: email,
destination: targets
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error(title, $("<pre/>").text(r));
show_aliases()
aliases_reset_form();
},
function(r) {
show_modal_error(title, r);
});
return false;
}
function aliases_reset_form() {
$("#addaliasEmail").prop('disabled', false);
$("#addaliasEmail").val('')
$("#addaliasTargets").val('')
$('#alias-cancel').addClass('hidden');
$('#add-alias-button').text('Add Alias');
is_alias_add_update = false;
}
function aliases_edit(elem) {
var email = $(elem).parents('tr').attr('data-email');
var targetdivs = $(elem).parents('tr').find('.target div');
var targets = "";
for (var i = 0; i < targetdivs.length; i++)
targets += $(targetdivs[i]).text() + "\n";
is_alias_add_update = true;
$('#alias-cancel').removeClass('hidden');
$("#addaliasEmail").prop('disabled', true);
$("#addaliasEmail").val(email);
$("#addaliasTargets").val(targets);
$('#add-alias-button').text('Update');
$('body').animate({ scrollTop: 0 })
}
function aliases_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Remove Alias",
"Remove " + email + "?",
"Remove",
function() {
api( api(
"/mail/aliases/remove", "/mail/aliases",
"GET",
{ format: 'json' },
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);
$('#alias_table tbody').append(hdr);
for (var k = 0; k < r[i].aliases.length; k++) {
var alias = r[i].aliases[k];
var n = $("#alias-template").clone();
n.attr('id', '');
if (alias.required) n.addClass('alias-required');
n.attr('data-email', alias.source);
n.find('td.email').text(alias.source)
for (var j = 0; j < alias.destination.length; j++)
n.find('td.target').append($("<div></div>").text(alias.destination[j]))
$('#alias_table tbody').append(n);
}
}
})
$(function() {
$('#alias_type_buttons button').off('click').click(function() {
$('#alias_type_buttons button').removeClass('active');
$(this).addClass('active');
if ($(this).text() == "Regular") {
$('#addaliasEmail').attr('type', 'email');
$('#addaliasEmail').attr('placeholder', 'incoming email address (you@yourdomain.com)');
$('#alias_catchall_info').slideUp();
} else {
$('#addaliasEmail').attr('type', 'text');
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (@yourdomain.com)');
$('#alias_catchall_info').slideDown();
}
})
})
}
var is_alias_add_update = false;
function do_add_alias() {
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
var email = $("#addaliasEmail").val();
var targets = $("#addaliasTargets").val();
api(
"/mail/aliases/add",
"POST", "POST",
{ {
source: email update_if_exists: is_alias_add_update ? '1' : '0',
source: email,
destination: targets
}, },
function(r) { function(r) {
// Responses are multiple lines of pre-formatted text. // Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r)); show_modal_error(title, $("<pre/>").text(r));
show_aliases(); show_aliases()
aliases_reset_form();
},
function(r) {
show_modal_error(title, r);
}); });
}); return false;
} }
function scroll_top() { function aliases_reset_form() {
$('html, body').animate({ $("#addaliasEmail").prop('disabled', false);
scrollTop: $("#panel_aliases").offset().top $("#addaliasEmail").val('')
}, 1000); $("#addaliasTargets").val('')
} $('#alias-cancel').addClass('hidden');
</script> $('#add-alias-button').text('Add Alias');
is_alias_add_update = false;
}
function aliases_edit(elem) {
var email = $(elem).parents('tr').attr('data-email');
var targetdivs = $(elem).parents('tr').find('.target div');
var targets = "";
for (var i = 0; i < targetdivs.length; i++)
targets += $(targetdivs[i]).text() + "\n";
is_alias_add_update = true;
$('#alias-cancel').removeClass('hidden');
$("#addaliasEmail").prop('disabled', true);
$("#addaliasEmail").val(email);
$("#addaliasTargets").val(targets);
$('#add-alias-button').text('Update');
$('body').animate({ scrollTop: 0 })
}
function aliases_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Remove Alias",
"Remove " + email + "?",
"Remove",
function() {
api(
"/mail/aliases/remove",
"POST",
{
source: email
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_aliases();
});
});
}
</script>
</div>

View File

@ -1,99 +1,105 @@
<style> <div class="container-fluid">
</style> <div class="row">
<div class="col-sm-12">
<h2>Custom DNS</h2> <h2>Custom DNS</h2>
<p class="text-warning">This is an advanced configuration page.</p> <p class="text-warning">This is an advanced configuration page.</p>
<p>It is possible to set custom DNS records on domains hosted here.</p> <p>It is possible to set custom DNS records on domains hosted here.</p>
<h3>Using a Secondary Nameserver</h3> <h3>Using a Secondary Nameserver</h3>
<p>If your TLD requires you to have two separate nameservers, you can either set up a secondary (aka &ldquo;slave&rdquo;) nameserver or, alternatively, set up <a href="#" onclick="return show_panel('external_dns')">external DNS</a> and ignore the DNS server on this box. If you choose to use a seconday/slave nameserver, you must find a seconday/slave nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday/slave nameserver service, enter the hostname of <em>their</em> secondary nameserver:</p> <p>If your TLD requires you to have two separate nameservers, you can either set up a secondary (aka &ldquo;slave&rdquo;) nameserver or, alternatively, set up <a href="#" onclick="return show_panel('external_dns')">external DNS</a> and ignore the DNS server on this box. If you choose to use a seconday/slave nameserver, you must find a seconday/slave nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday/slave nameserver service, enter the hostname of <em>their</em> secondary nameserver:</p>
<form class="form-horizontal" role="form" onsubmit="do_set_secondary_dns(); return false;"> <form class="form-horizontal" role="form" onsubmit="do_set_secondary_dns(); return false;">
<div class="form-group"> <div class="form-group">
<label for="secondarydnsHostname" class="col-sm-1 control-label">Hostname</label> <label for="secondarydnsHostname" class="col-sm-1 control-label">Hostname</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" id="secondarydnsHostname" placeholder="ns1.cloudprovider.com"> <input type="text" class="form-control" id="secondarydnsHostname" placeholder="ns1.cloudprovider.com">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
<div id="secondarydns-clear-instructions" class="form-group" style="display: none">
<div class="col-sm-offset-1 col-sm-11">
<p class="small">Clear the input field above and click Update to use this machine itself as secondary DNS, which is the default/normal setup.</p>
</div>
</div>
</form>
<h3>Custom DNS API</h3>
<p>Use your box&rsquo;s DNS API to set custom DNS records on domains hosted here. For instance, you can create your own dynamic DNS service.</p>
<p>Send a POST request like this:</p>
<pre>curl -d "" --user {email}:{password} https://{{hostname}}/admin/dns/set/<b>qname</b>[/<b>rtype</b>[/<b>value</b>]]</pre>
<h4>HTTP POST parameters</h4>
<table class="table">
<thead><th>Parameter</th> <th>Value</th></thead>
<tr><td>email</td> <td>The email address of any administrative user here.</td></tr>
<tr><td>password</td> <td>That user&rsquo;s password.</td></tr>
<tr><td>qname</td> <td>The fully qualified domain name for the record you are trying to set.</td></tr>
<tr><td>rtype</td> <td>The resource type. <code>A</code> if omitted. Possible values: <code>A</code> (an IPv4 address), <code>AAAA</code> (an IPv6 address), <code>TXT</code> (a text string), or <code>CNAME</code> (an alias, which is a fully qualified domain name).</td></tr>
<tr><td>value</td> <td>The new record&rsquo;s value. If omitted, the IPv4 address of the remote host is used. This is handy for dynamic DNS! To delete a record, use &ldquo;__delete__&rdquo;.</td></tr>
</table>
<p style="margin-top: 1em">Note that <code>-d ""</code> is merely to ensure curl sends a POST request. You do not need to put anything inside the quotes. You can also pass the value using typical form encoding in the POST body.</p>
<p>Strict <a href="http://tools.ietf.org/html/rfc4408">SPF</a> and <a href="https://datatracker.ietf.org/doc/draft-kucherawy-dmarc-base/?include_text=1">DMARC</a> records will be added to all custom domains unless you override them.</p>
<h4>Examples:</h4>
<pre># sets laptop.mydomain.com to point to the IP address of the machine you are executing curl on
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/laptop.mydomain.com
# sets an alias
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/cname/bar.mydomain.com
# clears the alias
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/bar.mydomain.com/cname/__delete__
# sets a TXT record using the alternate value syntax
curl -d "value=something%20here" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/txt
</pre>
</div>
</div> </div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
<div id="secondarydns-clear-instructions" class="form-group" style="display: none">
<div class="col-sm-offset-1 col-sm-11">
<p class="small">Clear the input field above and click Update to use this machine itself as secondary DNS, which is the default/normal setup.</p>
</div>
</div>
</form>
<h3>Custom DNS API</h3> <script>
function show_custom_dns() {
api(
"/dns/secondary-nameserver",
"GET",
{ },
function(data) {
$('#secondarydnsHostname').val(data.hostname ? data.hostname : '');
$('#secondarydns-clear-instructions').toggle(data.hostname != null);
});
}
<p>Use your box&rsquo;s DNS API to set custom DNS records on domains hosted here. For instance, you can create your own dynamic DNS service.</p> function do_set_secondary_dns() {
api(
"/dns/secondary-nameserver",
"POST",
{
hostname: $('#secondarydnsHostname').val()
},
function(data) {
if (data == "") return; // nothing updated
show_modal_error("Secondary DNS", $("<pre/>").text(data));
$('#secondarydns-clear-instructions').slideDown();
},
function(err) {
show_modal_error("Secondary DNS", $("<pre/>").text(err));
});
}
</script>
<p>Send a POST request like this:</p> </div>
<pre>curl -d "" --user {email}:{password} https://{{hostname}}/admin/dns/set/<b>qname</b>[/<b>rtype</b>[/<b>value</b>]]</pre>
<h4>HTTP POST parameters</h4>
<table class="table">
<thead><th>Parameter</th> <th>Value</th></thead>
<tr><td>email</td> <td>The email address of any administrative user here.</td></tr>
<tr><td>password</td> <td>That user&rsquo;s password.</td></tr>
<tr><td>qname</td> <td>The fully qualified domain name for the record you are trying to set.</td></tr>
<tr><td>rtype</td> <td>The resource type. <code>A</code> if omitted. Possible values: <code>A</code> (an IPv4 address), <code>AAAA</code> (an IPv6 address), <code>TXT</code> (a text string), or <code>CNAME</code> (an alias, which is a fully qualified domain name).</td></tr>
<tr><td>value</td> <td>The new record&rsquo;s value. If omitted, the IPv4 address of the remote host is used. This is handy for dynamic DNS! To delete a record, use &ldquo;__delete__&rdquo;.</td></tr>
</table>
<p style="margin-top: 1em">Note that <code>-d ""</code> is merely to ensure curl sends a POST request. You do not need to put anything inside the quotes. You can also pass the value using typical form encoding in the POST body.</p>
<p>Strict <a href="http://tools.ietf.org/html/rfc4408">SPF</a> and <a href="https://datatracker.ietf.org/doc/draft-kucherawy-dmarc-base/?include_text=1">DMARC</a> records will be added to all custom domains unless you override them.</p>
<h4>Examples:</h4>
<pre># sets laptop.mydomain.com to point to the IP address of the machine you are executing curl on
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/laptop.mydomain.com
# sets an alias
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/cname/bar.mydomain.com
# clears the alias
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/bar.mydomain.com/cname/__delete__
# sets a TXT record using the alternate value syntax
curl -d "value=something%20here" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/txt
</pre>
<script>
function show_custom_dns() {
api(
"/dns/secondary-nameserver",
"GET",
{ },
function(data) {
$('#secondarydnsHostname').val(data.hostname ? data.hostname : '');
$('#secondarydns-clear-instructions').toggle(data.hostname != null);
});
}
function do_set_secondary_dns() {
api(
"/dns/secondary-nameserver",
"POST",
{
hostname: $('#secondarydnsHostname').val()
},
function(data) {
if (data == "") return; // nothing updated
show_modal_error("Secondary DNS", $("<pre/>").text(data));
$('#secondarydns-clear-instructions').slideDown();
},
function(err) {
show_modal_error("Secondary DNS", $("<pre/>").text(err));
});
}
</script>

View File

@ -1,77 +1,87 @@
<style> <div class="container-fluid">
#external_dns_settings .heading td {
font-weight: bold;
font-size: 120%;
padding-top: 1.5em;
}
#external_dns_settings .heading.first td {
border-top: none;
padding-top: 0;
}
#external_dns_settings .values td {
border: 0;
padding-top: .75em;
padding-bottom: 0;
max-width: 50vw;
word-wrap: break-word;
}
#external_dns_settings .explanation td {
border: 0;
padding-top: .5em;
padding-bottom: .75em;
font-style: italic;
font-size: 95%;
color: #777;
}
</style>
<h2>External DNS</h2> <style>
#external_dns_settings .heading td {
<p class="text-warning">This is an advanced configuration page.</p> font-weight: bold;
font-size: 120%;
<p>Although your box is configured to serve its own DNS, it is possible to host your DNS elsewhere &mdash; such as in the DNS control panel provided by your domain name registrar or virtual cloud provider &mdash; by copying the DNS zone information shown in the table below into your external DNS server&rsquo;s control panel.</p> padding-top: 1.5em;
<p>If you do so, you are responsible for keeping your DNS entries up to date! If you previously enabled DNSSEC on your domain name by setting a DS record at your registrar, you will likely have to turn it off before changing nameservers.</p>
<table id="external_dns_settings" class="table">
<thead>
<tr>
<th>QName</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
function show_external_dns() {
$('#external_dns_settings tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/dns/dump",
"GET",
{ },
function(zones) {
$('#external_dns_settings tbody').html("");
for (var j = 0; j < zones.length; j++) {
var h = $("<tr class='heading'><td colspan='3'></td></tr>");
h.find("td").text(zones[j][0]);
$('#external_dns_settings tbody').append(h);
var r = zones[j][1];
for (var i = 0; i < r.length; i++) {
var n = $("<tr class='values'><td class='qname'/><td class='rtype'/><td class='value'/></tr>");
n.find('.qname').text(r[i].qname);
n.find('.rtype').text(r[i].rtype);
n.find('.value').text(r[i].value);
$('#external_dns_settings tbody').append(n);
var n = $("<tr class='explanation'><td colspan='3'/></tr>");
n.find('td').text(r[i].explanation);
$('#external_dns_settings tbody').append(n);
} }
} #external_dns_settings .heading.first td {
}) border-top: none;
} padding-top: 0;
</script> }
#external_dns_settings .values td {
border: 0;
padding-top: .75em;
padding-bottom: 0;
max-width: 50vw;
word-wrap: break-word;
}
#external_dns_settings .explanation td {
border: 0;
padding-top: .5em;
padding-bottom: .75em;
font-style: italic;
font-size: 95%;
color: #777;
}
</style>
<div class="row">
<div class="col-sm-12">
<h2>External DNS</h2>
<p class="text-warning">This is an advanced configuration page.</p>
<p>Although your box is configured to serve its own DNS, it is possible to host your DNS elsewhere &mdash; such as in the DNS control panel provided by your domain name registrar or virtual cloud provider &mdash; by copying the DNS zone information shown in the table below into your external DNS server&rsquo;s control panel.</p>
<p>If you do so, you are responsible for keeping your DNS entries up to date! If you previously enabled DNSSEC on your domain name by setting a DS record at your registrar, you will likely have to turn it off before changing nameservers.</p>
<table id="external_dns_settings" class="table">
<thead>
<tr>
<th>QName</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<script>
function show_external_dns() {
$('#external_dns_settings tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/dns/dump",
"GET",
{ },
function(zones) {
$('#external_dns_settings tbody').html("");
for (var j = 0; j < zones.length; j++) {
var h = $("<tr class='heading'><td colspan='3'></td></tr>");
h.find("td").text(zones[j][0]);
$('#external_dns_settings tbody').append(h);
var r = zones[j][1];
for (var i = 0; i < r.length; i++) {
var n = $("<tr class='values'><td class='qname'/><td class='rtype'/><td class='value'/></tr>");
n.find('.qname').text(r[i].qname);
n.find('.rtype').text(r[i].rtype);
n.find('.value').text(r[i].value);
$('#external_dns_settings tbody').append(n);
var n = $("<tr class='explanation'><td colspan='3'/></tr>");
n.find('td').text(r[i].explanation);
$('#external_dns_settings tbody').append(n);
}
}
})
}
</script>
</div>

View File

@ -17,13 +17,43 @@
@import url(https://fonts.googleapis.com/css?family=Raleway:400,700); @import url(https://fonts.googleapis.com/css?family=Raleway:400,700);
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300); @import url(https://fonts.googleapis.com/css?family=Ubuntu:300);
html { html {
overflow-y: scroll; overflow-y: scroll;
} }
html,
body {
height: 100%;
}
#wrap {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -50px;
}
#push,
#footer {
height: 50px;
}
@media (max-width: 767px) {
#footer {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}
}
body { body {
padding-top: 50px; padding-top: 50px;
padding-bottom: 20px; }
footer {
padding-top: 10px;
font: bold italic medium;
border-top: 1px solid #e5e5e5;
} }
p { p {
@ -61,9 +91,9 @@
margin: 1.5em 0; margin: 1.5em 0;
} }
ol li { ol li {
margin-bottom: 1em; margin-bottom: 1em;
} }
</style> </style>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<style> <style>
@ -73,99 +103,111 @@
<!--[if lt IE 7]> <!--[if lt IE 7]>
<p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p> <p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p>
<![endif]--> <![endif]-->
<div class="navbar navbar-inverse navbar-fixed-top"> <div id="wrap">
<div class="container"> <div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-header"> <div class="container">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <div class="navbar-header">
<span class="icon-bar"></span> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> <span class="icon-bar"></span>
<a class="navbar-brand" href="#">{{hostname}}</a> </button>
<a class="navbar-brand" href="#">{{hostname}}</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
<li class="divider"></li>
<li class="dropdown-header">Super Advanced Options</li>
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></li>
<li><a href="#users" onclick="return show_panel(this);">Users</a></li>
<li><a href="#aliases" onclick="return show_panel(this);">Aliases</a></li>
</ul>
</li>
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
</ul>
</div><!--/.navbar-collapse -->
</div>
</div>
<div class="container">
<div class="row">
<div class="col-sm-12">
<div id="panel_system_status" class="panel">
{% include "system-status.html" %}
</div>
<div id="panel_system_backup" class="panel">
{% include "system-backup.html" %}
</div>
<div id="panel_external_dns" class="panel">
{% include "external-dns.html" %}
</div>
<div id="panel_custom_dns" class="panel">
{% include "custom-dns.html" %}
</div>
<div id="panel_login" class="panel">
{% include "login.html" %}
</div>
<div id="panel_mail-guide" class="panel">
{% include "mail-guide.html" %}
</div>
<div id="panel_users" class="panel">
{% include "users.html" %}
</div>
<div id="panel_aliases" class="panel">
{% include "aliases.html" %}
</div>
<div id="panel_sync_guide" class="panel">
{% include "sync-guide.html" %}
</div>
<div id="panel_web" class="panel">
{% include "web.html" %}
</div>
<div id="panel_ssl" class="panel">
{% include "ssl.html" %}
</div>
</div>
</div>
<div id="push"></div>
</div>
</div>
<div id="footer">
<div class="container">
<div class="row">
<div class="col-sm-12">
<footer>
<p>This is a <a href="https://mailinabox.email">Mail-in-a-Box</a>.</p>
</footer>
</div>
</div>
</div>
</div> </div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
<li class="divider"></li>
<li class="dropdown-header">Super Advanced Options</li>
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></li>
<li><a href="#users" onclick="return show_panel(this);">Users</a></li>
<li><a href="#aliases" onclick="return show_panel(this);">Aliases</a></li>
</ul>
</li>
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
</ul>
</div><!--/.navbar-collapse -->
</div>
</div>
<div class="container-fluid">
<div id="panel_system_status" class="container panel">
{% include "system-status.html" %}
</div>
<div id="panel_system_backup" class="container panel">
{% include "system-backup.html" %}
</div>
<div id="panel_external_dns" class="container panel">
{% include "external-dns.html" %}
</div>
<div id="panel_custom_dns" class="container panel">
{% include "custom-dns.html" %}
</div>
<div id="panel_login" class="panel">
{% include "login.html" %}
</div>
<div id="panel_mail-guide" class="container panel">
{% include "mail-guide.html" %}
</div>
<div id="panel_users" class="container panel">
{% include "users.html" %}
</div>
<div id="panel_aliases" class="container panel">
{% include "aliases.html" %}
</div>
<div id="panel_sync_guide" class="container panel">
{% include "sync-guide.html" %}
</div>
<div id="panel_web" class="container panel">
{% include "web.html" %}
</div>
<div id="panel_ssl" class="container panel">
{% include "ssl.html" %}
</div>
<hr>
<footer>
<p>This is a <a href="https://mailinabox.email">Mail-in-a-Box</a>.</p>
</footer>
</div> <!-- /container -->
<div id="ajax_loading_indicator" style="display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; text-align: center; background-color: rgba(255,255,255,.75)"> <div id="ajax_loading_indicator" style="display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; text-align: center; background-color: rgba(255,255,255,.75)">
<div style="margin: 20% auto"> <div style="margin: 20% auto">

View File

@ -1,118 +1,128 @@
<h1 style="margin: 1em; text-align: center">{{hostname}}</h1> <div class="container-fluid">
<div class="row">
<div class="col-sm-12">
{% if no_admins_exist %} <h1 style="margin: 1em; text-align: center">{{hostname}}</h1>
<div class="container">
<div class="col-md-offset-2 col-md-8">
<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 your@emailaddress.com</pre>
<hr>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-sm-offset-2 col-sm-8 col-md-offset-3 col-md-6 col-lg-offset-4 col-lg-4">
<center>
<p style="margin: 2em">Log in here for your Mail-in-a-Box control panel.</p>
</center>
<form class="form-horizontal" role="form" onsubmit="do_login(); return false;">
<div class="form-group">
<label for="inputEmail3" class="col-sm-2 control-label">Email</label>
<div class="col-sm-10">
<input name="email" type="email" class="form-control" id="loginEmail" placeholder="Email">
</div>
</div>
<div class="form-group">
<label for="inputPassword3" class="col-sm-2 control-label">Password</label>
<div class="col-sm-10">
<input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input name='remember' type="checkbox" id="loginRemember"> Remember me
</label>
</div> </div>
</div>
</div> </div>
<div class="form-group"> {% if no_admins_exist %}
<div class="col-sm-offset-2 col-sm-10"> <div class="row">
<button type="submit" class="btn btn-default">Sign in</button> <div class="col-md-offset-2 col-md-8">
</div>
<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 your@emailaddress.com</pre>
<hr>
</div>
</div> </div>
</form> {% endif %}
</div> <div class="row">
</div> <div class="col-sm-12 col-md-offset-2 col-md-10 col-lg-offset-3 col-lg-6">
<center>
<p style="margin: 2em">Log in here for your Mail-in-a-Box control panel.</p>
</center>
<script> <form class="form-horizontal" role="form" onsubmit="do_login(); return false;">
function do_login() { <div class="form-group">
if ($('#loginEmail').val() == "") { <label for="inputEmail3" class="col-sm-2 control-label">Email</label>
show_modal_error("Login Failed", "Enter your email address.") <div class="col-sm-10">
return false; <input name="email" type="email" class="form-control" id="loginEmail" placeholder="Email">
} </div>
if ($('#loginPassword').val() == "") { </div>
show_modal_error("Login Failed", "Enter your email password.") <div class="form-group">
return false; <label for="inputPassword3" class="col-sm-2 control-label">Password</label>
} <div class="col-sm-10">
<input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<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-2 col-sm-10">
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
// Exchange the email address & password for an API key. </div>
api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()] </div>
api( <script>
"/me", function do_login() {
"GET", if ($('#loginEmail').val() == "") {
{ }, show_modal_error("Login Failed", "Enter your email address.")
function(response){ return false;
// This API call always succeeds. It returns a JSON object indicating }
// whether the request was authenticated or not. if ($('#loginPassword').val() == "") {
if (response.status != "authorized") { show_modal_error("Login Failed", "Enter your email password.")
// Show why the login failed. return false;
show_modal_error("Login Failed", response.reason)
// Reset any saved credentials.
do_logout();
} else {
// Login succeeded.
// Save the new credentials.
api_credentials = [response.api_key, ""];
// Try to wipe the username/password information.
$('#loginEmail').val('');
$('#loginPassword').val('');
// 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(":"));
}
} }
// Open the next panel the user wants to go to. Do this after the XHR response // Exchange the email address & password for an API key.
// is over so that we don't start a new XHR request while this one is finishing, api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()]
// which confuses the loading indicator.
setTimeout(function() { show_panel(!switch_back_to_panel ? 'system_status' : switch_back_to_panel) }, 300);
}
})
}
function do_logout() { api(
api_credentials = ["", ""]; "/me",
if (typeof localStorage != 'undefined') "GET",
localStorage.removeItem("miab-cp-credentials"); { },
if (typeof sessionStorage != 'undefined') function(response){
sessionStorage.removeItem("miab-cp-credentials"); // This API call always succeeds. It returns a JSON object indicating
show_panel('login'); // whether the request was authenticated or not.
} if (response.status != "authorized") {
</script> // Show why the login failed.
show_modal_error("Login Failed", response.reason)
// Reset any saved credentials.
do_logout();
} else {
// Login succeeded.
// Save the new credentials.
api_credentials = [response.api_key, ""];
// Try to wipe the username/password information.
$('#loginEmail').val('');
$('#loginPassword').val('');
// 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(":"));
}
}
// 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 ? 'system_status' : switch_back_to_panel) }, 300);
}
})
}
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');
}
</script>
</div>

View File

@ -1,10 +1,17 @@
<style>#panel_mail-guide table.table { width: auto; margin-left: .5em; }</style> <div class="container-fluid">
<div class="container"> <style>#panel_mail-guide table.table { width: auto; margin-left: .5em; }</style>
<h2 style="margin-bottom: 0">Checking and Sending Mail</h2>
<div class="row">
<div class="col-sm-12">
<h2 style="margin-bottom: 0">Checking and Sending Mail</h2>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<h3>How to log in</h3> <h3>How to log in</h3>
<p>Your username and password are the same no matter how you check your mail:</p> <p>Your username and password are the same no matter how you check your mail:</p>
@ -28,41 +35,45 @@
<li>On iOS devices, look for Exchange or ActiveSync.</li> <li>On iOS devices, look for Exchange or ActiveSync.</li>
</ul> </ul>
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<h4>IMAP/SMTP settings</h4>
<p>This method is preferred on Android devices but is not available on iOS devices.</p> <h4>IMAP/SMTP settings</h4>
<p>Your mail server is <strong>{{hostname}}</strong>. Use the following settings when prompted:</p> <p>This method is preferred on Android devices but is not available on iOS devices.</p>
<table class="table"> <p>Your mail server is <strong>{{hostname}}</strong>. Use the following settings when prompted:</p>
<thead>
<tr><th>Protocol</th> <th>Port</th> <th>Options</th></tr>
</thead>
<tr><th>IMAP</th> <td>993</td> <td>SSL</td></tr>
<tr><th>SMTP</th> <td>587</td> <td>STARTTLS <span>(&ldquo;always&rdquo; or &ldquo;required&rdquo;, if prompted)</span></td></tr>
</table>
<p>In addition to setting up your email, you&rsquo;ll also need to set up <a href="#sync_guide" onclick="return show_panel(this);">contacts and calendar synchronization</a> separately.</p> <table class="table">
</div> <thead>
<tr><th>Protocol</th> <th>Port</th> <th>Options</th></tr>
</thead>
<tr><th>IMAP</th> <td>993</td> <td>SSL</td></tr>
<tr><th>SMTP</th> <td>587</td> <td>STARTTLS <span>(&ldquo;always&rdquo; or &ldquo;required&rdquo;, if prompted)</span></td></tr>
</table>
<div class="col-lg-6"> <p>In addition to setting up your email, you&rsquo;ll also need to set up <a href="#sync_guide" onclick="return show_panel(this);">contacts and calendar synchronization</a> separately.</p>
<h4>Exchange/ActiveSync settings</h4>
<p>On iOS devices and devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, set up your mail as an Exchange or ActiveSync server. Use these settings when prompted:</p> </div>
<div class="col-lg-6">
<table class="table"> <h4>Exchange/ActiveSync settings</h4>
<tr><th>Server</th> <td>{{hostname}}</td></tr>
<tr><th>Options</th> <td>Secure Connection</td></tr> <p>On iOS devices and devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, set up your mail as an Exchange or ActiveSync server. Use these settings when prompted:</p>
</table>
<table class="table">
<tr><th>Server</th> <td>{{hostname}}</td></tr>
<tr><th>Options</th> <td>Secure Connection</td></tr>
</table>
<p>Your device should also provide a contacts list and calendar that syncs to this box when you use this method.</p>
</div>
</div>
<p>Your device should also provide a contacts list and calendar that syncs to this box when you use this method.</p>
</div>
</div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<h3>Other information about mail on your box</h3> <h3>Other information about mail on your box</h3>
<h4>Greylisting</h4> <h4>Greylisting</h4>
@ -70,6 +81,7 @@
<h4>Use this box to send as you</h4> <h4>Use this box to send as you</h4>
<p>Your box sets strict email sending policies for your domain names to make it harder for spam and other fraudulent mail to claim to be you. Only this machine is authorized to send email on behalf of your domain names. If you use any other service to send email as you, it will likely get spam filtered by recipients.</p> <p>Your box sets strict email sending policies for your domain names to make it harder for spam and other fraudulent mail to claim to be you. Only this machine is authorized to send email on behalf of your domain names. If you use any other service to send email as you, it will likely get spam filtered by recipients.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,118 +1,123 @@
<style> <div class="container-fluid">
</style> <div class="row">
<div class="col-sm-12">
<h2>SSL Certificates</h2> <h2>SSL Certificates</h2>
<h3>Certificate Status</h3> <h3>Certificate Status</h3>
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;">
<thead>
<tr>
<th>Domain</th>
<th>Certificate Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;"> <h3 id="ssl_install_header">Install SSL Certificate</h3>
<thead>
<tr>
<th>Domain</th>
<th>Certificate Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<h3 id="ssl_install_header">Install SSL Certificate</h3> <p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/cart/remove.aspx?itemid=47016639&i=i2">Namecheap&rsquo;s $9 certificate</a> or <a href="https://www.startssl.com/">StartSSL&rsquo;s free express lane</a>.</p>
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/cart/remove.aspx?itemid=47016639&i=i2">Namecheap&rsquo;s $9 certificate</a> or <a href="https://www.startssl.com/">StartSSL&rsquo;s free express lane</a>.</p> <p>Which domain are you getting an SSL certificate for?</p>
<p>Which domain are you getting an SSL certificate for?</p> <p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p> <div id="csr_info" style="display: none">
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
<div id="csr_info" style="display: none"> <pre id="ssl_csr"></pre>
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
<pre id="ssl_csr"></pre> <p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p> <p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
<p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p> <p style="margin-bottom: .5em">SSL certificate:</p>
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p style="margin-bottom: .5em">SSL certificate:</p> <p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p>
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----"></textarea></p> <p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----&#xA;-----BEGIN CERTIFICATE-----&#xA;more stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p> <p>After you paste in the information, click the install button.</p>
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----&#xA;-----BEGIN CERTIFICATE-----&#xA;more stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p>After you paste in the information, click the install button.</p> <button class="btn-primary" onclick="install_cert()">Install</button>
</div>
<button class="btn-primary" onclick="install_cert()">Install</button> </div>
</div> </div>
<script> <script>
function show_ssl() { function show_ssl() {
api( api(
"/web/domains", "/web/domains",
"GET", "GET",
{ {
}, },
function(domains) { function(domains) {
var tb = $('#ssl_domains tbody'); var tb = $('#ssl_domains tbody');
tb.text(''); tb.text('');
$('#ssldomain').html('<option value="">(select)</option>'); $('#ssldomain').html('<option value="">(select)</option>');
for (var i = 0; i < domains.length; i++) { for (var i = 0; i < domains.length; i++) {
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>"); var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
tb.append(row); tb.append(row);
row.attr('data-domain', domains[i].domain); row.attr('data-domain', domains[i].domain);
row.find('.domain a').text(domains[i].domain); row.find('.domain a').text(domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain); row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.addClass("text-" + domains[i].ssl_certificate[0]); row.addClass("text-" + domains[i].ssl_certificate[0]);
row.find('.status').text(domains[i].ssl_certificate[1]); row.find('.status').text(domains[i].ssl_certificate[1]);
if (domains[i].ssl_certificate[0] == "success") { if (domains[i].ssl_certificate[0] == "success") {
row.find('.actions a').addClass('btn-default').text('Replace Certificate'); row.find('.actions a').addClass('btn-default').text('Replace Certificate');
} else { } else {
row.find('.actions a').addClass('btn-primary').text('Install Certificate'); row.find('.actions a').addClass('btn-primary').text('Install Certificate');
} }
$('#ssldomain').append($('<option>').text(domains[i].domain)); $('#ssldomain').append($('<option>').text(domains[i].domain));
} }
}); });
} }
function ssl_install(elem) { function ssl_install(elem) {
var domain = $(elem).parents('tr').attr('data-domain'); var domain = $(elem).parents('tr').attr('data-domain');
$('#ssldomain').val(domain); $('#ssldomain').val(domain);
$('#csr_info').slideDown(); $('#csr_info').slideDown();
$('#ssl_csr').text('Loading...'); $('#ssl_csr').text('Loading...');
show_csr(); show_csr();
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top }) $('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top })
return false; return false;
} }
function show_csr() { function show_csr() {
api( api(
"/ssl/csr/" + $('#ssldomain').val(), "/ssl/csr/" + $('#ssldomain').val(),
"POST", "POST",
{ {
}, },
function(data) { function(data) {
$('#ssl_csr').text(data); $('#ssl_csr').text(data);
}); });
} }
function install_cert() { function install_cert() {
api( api(
"/ssl/install", "/ssl/install",
"POST", "POST",
{ {
domain: $('#ssldomain').val(), domain: $('#ssldomain').val(),
cert: $('#ssl_paste_cert').val(), cert: $('#ssl_paste_cert').val(),
chain: $('#ssl_paste_chain').val() chain: $('#ssl_paste_chain').val()
}, },
function(status) { function(status) {
if (status == "") { if (status == "") {
show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); }); show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
} else { } else {
show_modal_error("SSL Certificate Installation", status); show_modal_error("SSL Certificate Installation", status);
} }
}); });
} }
</script> </script>
</div>

View File

@ -1,49 +1,55 @@
<div class="container"> <div class="container-fluid">
<h2>Contacts &amp; Calendar Synchronization</h2> <div class="row">
<div class="col-sm-12">
<p>This box can hold your contacts and calendar, just like it holds your email.</p> <h2>Contacts &amp; Calendar Synchronization</h2>
<hr> <p>This box can hold your contacts and calendar, just like it holds your email.</p>
<div class="row"> <hr>
<div class="col-sm-6">
<h4>In your browser</h4>
<p>You can edit your contacts and calendar from your web browser.</p> </div>
</div>
<div class="row">
<div class="col-sm-6">
<h4>In your browser</h4>
<table class="table"> <p>You can edit your contacts and calendar from your web browser.</p>
<thead><tr><th>For...</th> <th>Visit this URL</th></tr></thead>
<tr><th>Contacts</td> <td><a href="https://{{hostname}}/cloud/contacts">https://{{hostname}}/cloud/contacts</a></td></tr>
<tr><th>Calendar</td> <td><a href="https://{{hostname}}/cloud/calendar">https://{{hostname}}/cloud/calendar</a></td></tr>
</table>
<p>Log in settings are the same as with <a href="#mail-guide" onclick="return show_panel(this);">mail</a>: your <table class="table">
complete email address and your mail password.</p> <thead><tr><th>For...</th> <th>Visit this URL</th></tr></thead>
</div> <tr><th>Contacts</td> <td><a href="https://{{hostname}}/cloud/contacts">https://{{hostname}}/cloud/contacts</a></td></tr>
<tr><th>Calendar</td> <td><a href="https://{{hostname}}/cloud/calendar">https://{{hostname}}/cloud/calendar</a></td></tr>
</table>
<div class="col-sm-6"> <p>Log in settings are the same as with <a href="#mail-guide" onclick="return show_panel(this);">mail</a>: your
<h4>On your mobile device</h4> complete email address and your mail password.</p>
<p>If you set up your <a href="#mail-guide" onclick="return show_panel(this);">mail</a> using Exchange/ActiveSync, </div>
your contacts and calendar may already appear on your device.</p> <div class="col-sm-6">
<p>Otherwise, here are some apps that can synchronize your contacts and calendar to your Android phone.</p> <h4>On your mobile device</h4>
<table class="table"> <p>If you set up your <a href="#mail-guide" onclick="return show_panel(this);">mail</a> using Exchange/ActiveSync,
<thead><tr><th>For...</th> <th>Use...</th></tr></thead> your contacts and calendar may already appear on your device.</p>
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/repository/browse/?fdfilter=dav&fdid=at.bitfire.davdroid">here</a>)</td></tr> <p>Otherwise, here are some apps that can synchronize your contacts and calendar to your Android phone.</p>
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free beta</a> (free)</td></tr>
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.89)</td></tr>
</table>
<p>Use the following settings:</p> <table class="table">
<thead><tr><th>For...</th> <th>Use...</th></tr></thead>
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/repository/browse/?fdfilter=dav&fdid=at.bitfire.davdroid">here</a>)</td></tr>
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free beta</a> (free)</td></tr>
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.89)</td></tr>
</table>
<table class="table"> <p>Use the following settings:</p>
<tr><td>Account Type</td> <td>CardDAV or CalDAV</td></tr>
<tr><td>Server Name</td> <td>{{hostname}}</td></tr> <table class="table">
<tr><td>Use SSL</td> <td>Yes</td></tr> <tr><td>Account Type</td> <td>CardDAV or CalDAV</td></tr>
<tr><td>Username</td> <td>Your complete email address.</td></tr> <tr><td>Server Name</td> <td>{{hostname}}</td></tr>
<tr><td>Password</td> <td>Your mail password.</td></tr> <tr><td>Use SSL</td> <td>Yes</td></tr>
</table> <tr><td>Username</td> <td>Your complete email address.</td></tr>
</div> <tr><td>Password</td> <td>Your mail password.</td></tr>
</div> </table>
</div>
</div>
</div> </div>

View File

@ -1,89 +1,99 @@
<style> <div class="container-fluid">
#backup-status th { text-align: center; }
#backup-status tr.full-backup td { font-weight: bold; }
</style>
<h2>Backup Status</h2> <style>
#backup-status th { text-align: center; }
#backup-status tr.full-backup td { font-weight: bold; }
</style>
<h3>Copying Backup Files</h3> <div class="row">
<div class="col-sm-12">
<p>The box makes an incremental backup each night. The backup is stored on the machine itself. You are responsible for copying the backup files off of the machine.</p> <h2>Backup Status</h2>
<p>Many cloud providers make this easy by allowing you to take snapshots of the machine's disk.</p> <h3>Copying Backup Files</h3>
<p>You can also use SFTP (FTP over SSH) to copy files from <tt id="backup-location"></tt>. These files are encrpyted, so they are safe to store anywhere. Copy the encryption password from <tt id="backup-encpassword-file"></tt> also but keep it in a safe location.</p> <p>The box makes an incremental backup each night. The backup is stored on the machine itself. You are responsible for copying the backup files off of the machine.</p>
<h3>Current Backups</h3> <p>Many cloud providers make this easy by allowing you to take snapshots of the machine's disk.</p>
<p>The backup directory currently contains the backups listed below. The total size on disk of the backups is <span id="backup-total-size"></span>.</p> <p>You can also use SFTP (FTP over SSH) to copy files from <tt id="backup-location"></tt>. These files are encrpyted, so they are safe to store anywhere. Copy the encryption password from <tt id="backup-encpassword-file"></tt> also but keep it in a safe location.</p>
<table id="backup-status" class="table" style="width: auto"> <h3>Current Backups</h3>
<thead>
<th colspan="2">When</th>
<th>Type</th>
<th>Size</th>
<th>Deleted in...</th>
</thead>
<tbody>
</tbody>
</table>
<p style="margin-top: 2em"><small>The size column in the table indicates the size of the encrpyted backup, but the total size on disk shown above includes storage for unencrpyted intermediate files.</small></p> <p>The backup directory currently contains the backups listed below. The total size on disk of the backups is <span id="backup-total-size"></span>.</p>
<script> <table id="backup-status" class="table" style="width: auto">
function nice_size(bytes) { <thead>
var powers = ['bytes', 'KB', 'MB', 'GB', 'TB']; <th colspan="2">When</th>
while (true) { <th>Type</th>
if (powers.length == 1) break; <th>Size</th>
if (bytes < 1000) break; <th>Deleted in...</th>
bytes /= 1024; </thead>
powers.shift(); <tbody>
} </tbody>
// round to have three significant figures but at most one decimal place </table>
if (bytes >= 100)
bytes = Math.round(bytes)
else
bytes = Math.round(bytes*10)/10;
return bytes + " " + powers[0];
}
function show_system_backup() { <p style="margin-top: 2em"><small>The size column in the table indicates the size of the encrpyted backup, but the total size on disk shown above includes storage for unencrpyted intermediate files.</small></p>
$('#backup-status tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/backup/status",
"GET",
{ },
function(r) {
$('#backup-location').text(r.encdirectory);
$('#backup-encpassword-file').text(r.encpwfile);
$('#backup-status tbody').html(""); </div>
var total_disk_size = 0; </div>
if (r.backups.length == 0) { <script>
var tr = $('<tr><td colspan="3">No backups have been made yet.</td></tr>'); function nice_size(bytes) {
$('#backup-status tbody').append(tr); var powers = ['bytes', 'KB', 'MB', 'GB', 'TB'];
while (true) {
if (powers.length == 1) break;
if (bytes < 1000) break;
bytes /= 1024;
powers.shift();
} }
// round to have three significant figures but at most one decimal place
if (bytes >= 100)
bytes = Math.round(bytes)
else
bytes = Math.round(bytes*10)/10;
return bytes + " " + powers[0];
}
for (var i = 0; i < r.backups.length; i++) { function show_system_backup() {
var b = r.backups[i]; $('#backup-status tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
var tr = $('<tr/>'); api(
if (b.full) tr.addClass("full-backup"); "/system/backup/status",
tr.append( $('<td/>').text(b.date_str + " " + r.tz) ); "GET",
tr.append( $('<td/>').text(b.date_delta + " ago") ); { },
tr.append( $('<td/>').text(b.full ? "full" : "increment") ); function(r) {
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.encsize)) ); $('#backup-location').text(r.encdirectory);
if (b.deleted_in) $('#backup-encpassword-file').text(r.encpwfile);
tr.append( $('<td/>').text(b.deleted_in) );
else
tr.append( $('<td class="text-muted">n/a</td>') );
$('#backup-status tbody').append(tr);
total_disk_size += b.size; $('#backup-status tbody').html("");
total_disk_size += b.encsize; var total_disk_size = 0;
}
$('#backup-total-size').text(nice_size(total_disk_size)); if (r.backups.length == 0) {
}) var tr = $('<tr><td colspan="3">No backups have been made yet.</td></tr>');
} $('#backup-status tbody').append(tr);
</script> }
for (var i = 0; i < r.backups.length; i++) {
var b = r.backups[i];
var tr = $('<tr/>');
if (b.full) tr.addClass("full-backup");
tr.append( $('<td/>').text(b.date_str + " " + r.tz) );
tr.append( $('<td/>').text(b.date_delta + " ago") );
tr.append( $('<td/>').text(b.full ? "full" : "increment") );
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.encsize)) );
if (b.deleted_in)
tr.append( $('<td/>').text(b.deleted_in) );
else
tr.append( $('<td class="text-muted">n/a</td>') );
$('#backup-status tbody').append(tr);
total_disk_size += b.size;
total_disk_size += b.encsize;
}
$('#backup-total-size').text(nice_size(total_disk_size));
})
}
</script>
</div>

View File

@ -1,86 +1,96 @@
<h2>System Status Checks</h2> <div class="container-fluid">
<style> <style>
#system-checks .heading td { #system-checks .heading td {
font-weight: bold; font-weight: bold;
font-size: 120%; font-size: 120%;
padding-top: 1.5em; padding-top: 1.5em;
}
#system-checks .heading.first td {
border-top: none;
padding-top: 0;
}
#system-checks .status-error td {
color: #733;
}
#system-checks .status-warning td {
color: #770;
}
#system-checks .status-ok td {
color: #040;
}
#system-checks div.extra {
display: none;
margin-top: 1em;
max-width: 50em;
word-wrap: break-word;
}
#system-checks a.showhide {
display: none;
font-size: 85%;
}
#system-checks .pre {
margin: 1em;
font-family: monospace;
white-space: pre-wrap;
}
</style>
<table id="system-checks" class="table" style="max-width: 60em">
<thead>
</thead>
<tbody>
</tbody>
</table>
<script>
function show_system_status() {
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/status",
"POST",
{ },
function(r) {
$('#system-checks tbody').html("");
for (var i = 0; i < r.length; i++) {
var n = $("<tr><td class='status'/><td class='message'><p style='margin: 0'/><div class='extra'/><a class='showhide' href='#'/></tr>");
if (i == 0) n.addClass('first')
if (r[i].type == "heading")
n.addClass(r[i].type)
else
n.addClass("status-" + r[i].type)
if (r[i].type == "ok") n.find('td.status').text("✓")
if (r[i].type == "error") n.find('td.status').text("✖")
if (r[i].type == "warning") n.find('td.status').text("?")
n.find('td.message p').text(r[i].text)
$('#system-checks tbody').append(n);
if (r[i].extra.length > 0) {
n.find('a.showhide').show().text("show more").click(function() {
$(this).hide();
$(this).parent().find('.extra').fadeIn();
return false;
});
} }
#system-checks .heading.first td {
for (var j = 0; j < r[i].extra.length; j++) { border-top: none;
padding-top: 0;
var m = $("<div/>").text(r[i].extra[j].text)
if (r[i].extra[j].monospace)
m.addClass("pre");
n.find('> td.message > div').append(m);
} }
} #system-checks .status-error td {
}) color: #733;
} }
</script> #system-checks .status-warning td {
color: #770;
}
#system-checks .status-ok td {
color: #040;
}
#system-checks div.extra {
display: none;
margin-top: 1em;
max-width: 50em;
word-wrap: break-word;
}
#system-checks a.showhide {
display: none;
font-size: 85%;
}
#system-checks .pre {
margin: 1em;
font-family: monospace;
white-space: pre-wrap;
}
</style>
<div class="row">
<div class="col-sm-12">
<h2>System Status Checks</h2>
<table id="system-checks" class="table" style="max-width: 60em">
<thead>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<script>
function show_system_status() {
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/status",
"POST",
{ },
function(r) {
$('#system-checks tbody').html("");
for (var i = 0; i < r.length; i++) {
var n = $("<tr><td class='status'/><td class='message'><p style='margin: 0'/><div class='extra'/><a class='showhide' href='#'/></tr>");
if (i == 0) n.addClass('first')
if (r[i].type == "heading")
n.addClass(r[i].type)
else
n.addClass("status-" + r[i].type)
if (r[i].type == "ok") n.find('td.status').text("✓")
if (r[i].type == "error") n.find('td.status').text("✖")
if (r[i].type == "warning") n.find('td.status').text("?")
n.find('td.message p').text(r[i].text)
$('#system-checks tbody').append(n);
if (r[i].extra.length > 0) {
n.find('a.showhide').show().text("show more").click(function() {
$(this).hide();
$(this).parent().find('.extra').fadeIn();
return false;
});
}
for (var j = 0; j < r[i].extra.length; j++) {
var m = $("<div/>").text(r[i].extra[j].text)
if (r[i].extra[j].monospace)
m.addClass("pre");
n.find('> td.message > div').append(m);
}
}
})
}
</script>
</div>

View File

@ -1,252 +1,261 @@
<h2>Users</h2> <div class="container-fluid">
<style> <style>
#user_table tr.account_inactive td.address { color: #888; text-decoration: line-through; } #user_table tr.account_inactive td.address { color: #888; text-decoration: line-through; }
#user_table .aliases { font-size: 90%; } #user_table .aliases { font-size: 90%; }
#user_table .aliases div:before { content: "⇖ "; } #user_table .aliases div:before { content: "⇖ "; }
#user_table .aliases div { } #user_table .aliases div { }
#user_table .actions { margin-top: .33em; font-size: 95%; } #user_table .actions { margin-top: .33em; font-size: 95%; }
#user_table .account_inactive .if_active { display: none; } #user_table .account_inactive .if_active { display: none; }
#user_table .account_active .if_inactive { display: none; } #user_table .account_active .if_inactive { display: none; }
</style> </style>
<h3>Add a mail user</h3> <div class="row">
<div class="col-sm-12">
<p>Add an email address to this system. This will create a new login username/password. (Use <a href="javascript:show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.)</p> <h2>Users</h2>
<form class="form-inline" role="form" onsubmit="return do_add_user(); return false;"> <h3>Add a mail user</h3>
<div class="form-group">
<label class="sr-only" for="adduserEmail">Email address</label>
<input type="email" class="form-control" id="adduserEmail" placeholder="Email Address">
</div>
<div class="form-group">
<label class="sr-only" for="adduserPassword">Password</label>
<input type="password" class="form-control" id="adduserPassword" placeholder="Password">
</div>
<div class="form-group">
<select class="form-control" id="adduserPrivs">
<option value="">Normal User</option>
<option value="admin">Administrator</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
<p style="margin-top: .5em"><small>
Passwords must be at least four characters and may not contain spaces.
Administrators get access to this control panel.
</small></p>
<h3>Existing mail users</h3> <p>Add an email address to this system. This will create a new login username/password. (Use <a href="javascript:show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.)</p>
<table id="user_table" class="table" style="width: auto">
<thead>
<tr>
<th width="50%">Email Address</th>
<th>Actions</th>
<th>Mailbox Size</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div style="display: none"> <form class="form-inline" role="form" onsubmit="return do_add_user(); return false;">
<table> <div class="form-group">
<tr id="user-template"> <label class="sr-only" for="adduserEmail">Email address</label>
<td class='address'> <input type="email" class="form-control" id="adduserEmail" placeholder="Email Address">
</td> </div>
<td class='actions'> <div class="form-group">
<span class='privs'> <label class="sr-only" for="adduserPassword">Password</label>
</span> <input type="password" class="form-control" id="adduserPassword" placeholder="Password">
</div>
<div class="form-group">
<select class="form-control" id="adduserPrivs">
<option value="">Normal User</option>
<option value="admin">Administrator</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
<p style="margin-top: .5em"><small>
Passwords must be at least four characters and may not contain spaces.
Administrators get access to this control panel.
</small></p>
<span class="if_active"> <h3>Existing mail users</h3>
<a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password"> <table id="user_table" class="table" style="width: auto">
set password <thead>
</a> <tr>
| <th width="50%">Email Address</th>
</span> <th>Actions</th>
<th>Mailbox Size</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<span class='add-privs'> <div style="display: none">
</span> <table>
<tr id="user-template">
<td class='address'>
</td>
<td class='actions'>
<span class='privs'>
</span>
<a href="#" onclick="users_remove(this); return false;" class='if_active' title="Archive Account"> <span class="if_active">
archive account <a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password">
</a> set password
</td> </a>
<td class='mailboxsize'> |
</td> </span>
</tr>
<tr id="user-extra-template">
<td colspan="3" style="border-top: 0; padding-top: 0">
<div class='if_inactive restore_info' style='color: #888; font-size: 90%'>To restore account, create a new account with this email address. Or to permanently delete the mailbox, delete the directory <tt></tt> on the machine.</div>
<div class='aliases' style='display: none'> </div> <span class='add-privs'>
</td> </span>
</tr>
</table>
</div>
<a href="#" onclick="users_remove(this); return false;" class='if_active' title="Archive Account">
archive account
</a>
</td>
<td class='mailboxsize'>
</td>
</tr>
<tr id="user-extra-template">
<td colspan="3" style="border-top: 0; padding-top: 0">
<div class='if_inactive restore_info' style='color: #888; font-size: 90%'>To restore account, create a new account with this email address. Or to permanently delete the mailbox, delete the directory <tt></tt> on the machine.</div>
<script> <div class='aliases' style='display: none'></div>
function show_users() { </td>
$('#user_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>") </tr>
api( </table>
"/mail/users", </div>
"GET",
{ format: 'json' },
function(r) {
$('#user_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><td><h4/></td></tr>");
hdr.find('h4').text(r[i].domain);
$('#user_table tbody').append(hdr);
for (var k = 0; k < r[i].users.length; k++) { </div>
var user = r[i].users[k]; </div>
var n = $("#user-template").clone(); <script>
var n2 = $("#user-extra-template").clone(); function show_users() {
n.attr('id', ''); $('#user_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
n2.attr('id', ''); api(
$('#user_table tbody').append(n); "/mail/users",
$('#user_table tbody').append(n2); "GET",
{ format: 'json' },
function(r) {
$('#user_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><td><h4/></td></tr>");
hdr.find('h4').text(r[i].domain);
$('#user_table tbody').append(hdr);
n.addClass("account_" + user.status); for (var k = 0; k < r[i].users.length; k++) {
n2.addClass("account_" + user.status); var user = r[i].users[k];
n.attr('data-email', user.email); var n = $("#user-template").clone();
n.find('.address').text(user.email) var n2 = $("#user-extra-template").clone();
n.find('.mailboxsize').text(nice_size(user.mailbox_size)) n.attr('id', '');
n2.find('.restore_info tt').text(user.mailbox); n2.attr('id', '');
$('#user_table tbody').append(n);
$('#user_table tbody').append(n2);
if (user.status == 'inactive') continue; n.addClass("account_" + user.status);
n2.addClass("account_" + user.status);
var add_privs = ["admin"]; n.attr('data-email', user.email);
n.find('.address').text(user.email)
n.find('.mailboxsize').text(nice_size(user.mailbox_size))
n2.find('.restore_info tt').text(user.mailbox);
for (var j = 0; j < user.privileges.length; j++) { if (user.status == 'inactive') continue;
var p = $("<span><b><span class='name'></span></b> (<a href='#' onclick='mod_priv(this, \"remove\"); return false;' title='Remove Privilege'>remove privilege</a>) |</span>");
p.find('span.name').text(user.privileges[j]);
n.find('.privs').append(p);
if (add_privs.indexOf(user.privileges[j]) >= 0)
add_privs.splice(add_privs.indexOf(user.privileges[j]), 1);
}
for (var j = 0; j < add_privs.length; j++) { var add_privs = ["admin"];
var p = $("<span><a href='#' onclick='mod_priv(this, \"add\"); return false;' title='Add Privilege'>make <span class='name'></span></a> | </span>");
p.find('span.name').text(add_privs[j]);
n.find('.add-privs').append(p);
}
if (user.aliases && user.aliases.length > 0) { for (var j = 0; j < user.privileges.length; j++) {
n2.find('.aliases').show(); var p = $("<span><b><span class='name'></span></b> (<a href='#' onclick='mod_priv(this, \"remove\"); return false;' title='Remove Privilege'>remove privilege</a>) |</span>");
for (var j = 0; j < user.aliases.length; j++) { p.find('span.name').text(user.privileges[j]);
n2.find('.aliases').append($("<div/>").text( n.find('.privs').append(p);
user.aliases[j][0] if (add_privs.indexOf(user.privileges[j]) >= 0)
+ (user.aliases[j][1].length > 0 ? " ⇐ " + user.aliases[j][1].join(", ") : "") add_privs.splice(add_privs.indexOf(user.privileges[j]), 1);
)) }
for (var j = 0; j < add_privs.length; j++) {
var p = $("<span><a href='#' onclick='mod_priv(this, \"add\"); return false;' title='Add Privilege'>make <span class='name'></span></a> | </span>");
p.find('span.name').text(add_privs[j]);
n.find('.add-privs').append(p);
}
if (user.aliases && user.aliases.length > 0) {
n2.find('.aliases').show();
for (var j = 0; j < user.aliases.length; j++) {
n2.find('.aliases').append($("<div/>").text(
user.aliases[j][0]
+ (user.aliases[j][1].length > 0 ? " ⇐ " + user.aliases[j][1].join(", ") : "")
))
}
}
} }
} }
} })
}
function do_add_user() {
var email = $("#adduserEmail").val();
var pw = $("#adduserPassword").val();
var privs = $("#adduserPrivs").val();
api(
"/mail/users/add",
"POST",
{
email: email,
password: pw,
privileges: privs
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Add User", $("<pre/>").text(r));
show_users()
},
function(r) {
show_modal_error("Add User", r);
});
return false;
}
function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Archive User",
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small></p>"),
"Set Password",
function() {
api(
"/mail/users/password",
"POST",
{
email: email,
password: $('#users_set_password_pw').val()
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Set Password", $("<pre/>").text(r));
},
function(r) {
show_modal_error("Set Password", r);
});
});
}
function users_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Archive User",
$("<p>Are you sure you want to archive <b>" + email + "</b>?</p> <p>The user's mailboxes will not be deleted (you can do that later), but the user will no longer be able to log into any services on this machine.</p>"),
"Archive",
function() {
api(
"/mail/users/remove",
"POST",
{
email: email
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_users();
},
function(r) {
show_modal_error("Remove User", r);
});
});
}
function mod_priv(elem, add_remove) {
var email = $(elem).parents('tr').attr('data-email');
var priv = $(elem).parents('td').find('.name').text();
// can't remove your own admin access
if (priv == "admin" && add_remove == "remove" && api_credentials != null && email == api_credentials[0]) {
show_modal_error("Modify Privileges", "You cannot remove the admin privilege from yourself.");
return;
} }
})
}
function do_add_user() { var add_remove1 = add_remove.charAt(0).toUpperCase() + add_remove.substring(1);
var email = $("#adduserEmail").val(); show_modal_confirm(
var pw = $("#adduserPassword").val(); "Modify Privileges",
var privs = $("#adduserPrivs").val(); "Are you sure you want to " + add_remove + " the " + priv + " privilege for <b>" + email + "</b>?",
api( add_remove1,
"/mail/users/add", function() {
"POST", api(
{ "/mail/users/privileges/" + add_remove,
email: email, "POST",
password: pw, {
privileges: privs email: email,
}, privilege: priv
function(r) { },
// Responses are multiple lines of pre-formatted text. function(r) {
show_modal_error("Add User", $("<pre/>").text(r)); show_users();
show_users() });
},
function(r) {
show_modal_error("Add User", r);
});
return false;
}
function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Archive User",
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small></p>"),
"Set Password",
function() {
api(
"/mail/users/password",
"POST",
{
email: email,
password: $('#users_set_password_pw').val()
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Set Password", $("<pre/>").text(r));
},
function(r) {
show_modal_error("Set Password", r);
}); });
}); }
} </script>
function users_remove(elem) { </div>
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Archive User",
$("<p>Are you sure you want to archive <b>" + email + "</b>?</p> <p>The user's mailboxes will not be deleted (you can do that later), but the user will no longer be able to log into any services on this machine.</p>"),
"Archive",
function() {
api(
"/mail/users/remove",
"POST",
{
email: email
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_users();
},
function(r) {
show_modal_error("Remove User", r);
});
});
}
function mod_priv(elem, add_remove) {
var email = $(elem).parents('tr').attr('data-email');
var priv = $(elem).parents('td').find('.name').text();
// can't remove your own admin access
if (priv == "admin" && add_remove == "remove" && api_credentials != null && email == api_credentials[0]) {
show_modal_error("Modify Privileges", "You cannot remove the admin privilege from yourself.");
return;
}
var add_remove1 = add_remove.charAt(0).toUpperCase() + add_remove.substring(1);
show_modal_confirm(
"Modify Privileges",
"Are you sure you want to " + add_remove + " the " + priv + " privilege for <b>" + email + "</b>?",
add_remove1,
function() {
api(
"/mail/users/privileges/" + add_remove,
"POST",
{
email: email,
privilege: priv
},
function(r) {
show_users();
});
});
}
</script>

View File

@ -1,102 +1,107 @@
<style> <div class="container-fluid">
</style> <div class="row">
<div class="col-sm-12">
<h2>Static Web Hosting</h2> <h2>Static Web Hosting</h2>
<p>This machine is serving a simple, static website at <a href="https://{{hostname}}">https://{{hostname}}</a> and at all domain names that you set up an email user or alias for.</p> <p>This machine is serving a simple, static website at <a href="https://{{hostname}}">https://{{hostname}}</a> and at all domain names that you set up an email user or alias for.</p>
<h3>Uploading web files</h3> <h3>Uploading web files</h3>
<p>You can replace the default website with your own HTML pages and other static files. This control panel won&rsquo;t help you design a website, but once you have <tt>.html</tt> files you can upload it following these instructions:</p> <p>You can replace the default website with your own HTML pages and other static files. This control panel won&rsquo;t help you design a website, but once you have <tt>.html</tt> files you can upload it following these instructions:</p>
<ol> <ol>
<li>Ensure that any domains you are publishing a website for have no problems on the <a href="#system_status" onclick="return show_panel(this);">Status Checks</a> page.</li> <li>Ensure that any domains you are publishing a website for have no problems on the <a href="#system_status" onclick="return show_panel(this);">Status Checks</a> page.</li>
<li>On your personal computer, install an SSH file transfer program such as <a href="https://filezilla-project.org/">FileZilla</a> or <a href="http://linuxcommand.org/man_pages/scp1.html">scp</a>.</li> <li>On your personal computer, install an SSH file transfer program such as <a href="https://filezilla-project.org/">FileZilla</a> or <a href="http://linuxcommand.org/man_pages/scp1.html">scp</a>.</li>
<li>Log in to this machine with the file transfer program. The server is <strong>{{hostname}}</strong>, the protocol is SSH or SFTP, and use the <strong>SSH login credentials</strong> that you used when you originally created this machine at your cloud host provider. This is <strong>not</strong> what you use to log in either for email or this control panel. Your SSH credentials probably involves a private key file.</li> <li>Log in to this machine with the file transfer program. The server is <strong>{{hostname}}</strong>, the protocol is SSH or SFTP, and use the <strong>SSH login credentials</strong> that you used when you originally created this machine at your cloud host provider. This is <strong>not</strong> what you use to log in either for email or this control panel. Your SSH credentials probably involves a private key file.</li>
<li>Upload your <tt>.html</tt> or other files to the directory <tt>{{storage_root}}/www/default</tt> on this machine. They will appear directly and immediately on the web.</li> <li>Upload your <tt>.html</tt> or other files to the directory <tt>{{storage_root}}/www/default</tt> on this machine. They will appear directly and immediately on the web.</li>
<li>The websites set up on this machine are listed in the table below with where to put the files for each website (if you have customized that, see next section).</li> <li>The websites set up on this machine are listed in the table below with where to put the files for each website (if you have customized that, see next section).</li>
<table id="web_domains_existing" class="table" style="margin-bottom: 2em; width: auto;"> <table id="web_domains_existing" class="table" style="margin-bottom: 2em; width: auto;">
<thead> <thead>
<tr> <tr>
<th>Site</th> <th>Site</th>
<th>Directory for Files</th> <th>Directory for Files</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
<li>If you want to have this box host a static website on a domain that is not listed in the table, create a dummy <a href="#users" onclick="return show_panel(this);">mail user</a> or <a href="#aliases" onclick="return show_panel(this);">alias</a> on the domain first.</li> <li>If you want to have this box host a static website on a domain that is not listed in the table, create a dummy <a href="#users" onclick="return show_panel(this);">mail user</a> or <a href="#aliases" onclick="return show_panel(this);">alias</a> on the domain first.</li>
</ol> </ol>
<h3>Different sites for different domains</h3> <h3>Different sites for different domains</h3>
<p>Create one of the directories shown in the table below to create a space for different files for one of the websites.</p> <p>Create one of the directories shown in the table below to create a space for different files for one of the websites.</p>
<p>After you create one of these directories, click <button id="web_update" class="btn btn-primary" onclick="do_web_update()">Web Update</button> to restart the box&rsquo;s web server so that it sees the new website file location.</p> <p>After you create one of these directories, click <button id="web_update" class="btn btn-primary" onclick="do_web_update()">Web Update</button> to restart the box&rsquo;s web server so that it sees the new website file location.</p>
<table id="web_domains_custom" class="table" style="margin-bottom: 2em; width: auto;"> <table id="web_domains_custom" class="table" style="margin-bottom: 2em; width: auto;">
<thead> <thead>
<tr> <tr>
<th>Site</th> <th>Site</th>
<th>Create Directory</th> <th>Create Directory</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
</div>
</div>
<script> <script>
function show_web() { function show_web() {
api( api(
"/web/domains", "/web/domains",
"GET", "GET",
{ {
}, },
function(domains) { function(domains) {
var tb = $('#web_domains_existing tbody'); var tb = $('#web_domains_existing tbody');
tb.text(''); tb.text('');
for (var i = 0; i < domains.length; i++) { for (var i = 0; i < domains.length; i++) {
var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt/></td></tr>"); var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt/></td></tr>");
tb.append(row); tb.append(row);
row.find('.domain a').text('https://' + domains[i].domain); row.find('.domain a').text('https://' + domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain); row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.find('.directory tt').text(domains[i].root); row.find('.directory tt').text(domains[i].root);
} }
tb = $('#web_domains_custom tbody'); tb = $('#web_domains_custom tbody');
tb.text(''); tb.text('');
for (var i = 0; i < domains.length; i++) { for (var i = 0; i < domains.length; i++) {
if (domains[i].root != domains[i].custom_root) { if (domains[i].root != domains[i].custom_root) {
var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt></td></tr>"); var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt></td></tr>");
tb.append(row); tb.append(row);
row.find('.domain a').text('https://' + domains[i].domain); row.find('.domain a').text('https://' + domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain); row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.find('.directory tt').text(domains[i].custom_root); row.find('.directory tt').text(domains[i].custom_root);
} }
} }
}); });
} }
function do_web_update() { function do_web_update() {
api( api(
"/web/update", "/web/update",
"POST", "POST",
{ {
}, },
function(data) { function(data) {
if (data == "") if (data == "")
data = "Nothing changed."; data = "Nothing changed.";
else else
data = $("<pre/>").text(data); data = $("<pre/>").text(data);
show_modal_error("Web Update", data, function() { show_web() }); show_modal_error("Web Update", data, function() { show_web() });
}); });
} }
</script> </script>
</div>