include changes from v0.53. Remove some POWER modifications to closer follow original mialinabox

This commit is contained in:
github@kiekerjan.isdronken.nl 2021-04-13 09:50:23 +02:00
parent 40adef2261
commit c24ca5abd4
39 changed files with 577 additions and 1063 deletions

View File

@ -12,9 +12,6 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.html]
indent_style = tab
[Makefile]
indent_style = tab
indent_size = 4

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ externals/
.env
.vagrant
api/docs/api-docs.html
mailinabox-ca.crt

View File

@ -1,13 +1,35 @@
CHANGELOG
=========
v0.53 (April 12, 2021)
----------------------
Software updates:
* Upgraded Roundcube to version 1.4.11 addressing a security issue, and its desktop notifications plugin.
* Upgraded Z-Push (for Exchange/ActiveSync) to version 2.6.2.
Control panel:
* Backblaze B2 is now a supported backup protocol.
* Fixed an issue in the daily mail reports.
* Sort the Custom DNS by zone and qname, and add an option to go back to the old sort order (creation order).
Mail:
* Enable sending DMARC failure reports to senders that request them.
Setup:
* Fixed error when upgrading from Nextcloud 13.
v0.52 (January 31, 2021)
------------------------
Software updates:
* Upgraded Roundcube to version 1.4.10.
* Upgraded zpush to 2.6.1.
* Upgraded Z-Push to 2.6.1.
Mail:

View File

@ -1,86 +1,14 @@
(Power) Mail-in-a-Box
=====================
This is not the original Mail-in-a-Box. See https://github.com/mail-in-a-box/mailinabox for the real deal! I made a number of modifications to to:
- add geoipblocking on the admin web console
- add geoipblocking for ssh access
- make fail2ban a more stricter
- add fail2ban filter for web scanners
- other small stuff
## Installation
Original mailinabox content starts here:
- **PRE-REQUISITES:** Debian 10 (Buster) or Ubuntu 20.04 LTS fresh installation
Update packages:
```sh
sudo apt update
sudo apt full-upgrade
```
Make sure that the `en_US.UTF-8` locale exists and is set as primary (this depends on the image you use)
```sh
sudo apt install locales
sudo dpkg-reconfigure locales
```
Install Power-Mail-in-a-Box (short link)
```sh
curl -L https://dvn.pt/powermiab | sudo bash
```
If that doesn't work:
```sh
curl https://raw.githubusercontent.com/ddavness/power-mailinabox/master/setup/bootstrap.sh | sudo bash
```
## Current Version: v0.52.POWER.0 (Tracking v0.52)
This is a fork of MiaB (duh), hacked and tuned to my needs:
✅ - **Done**
👨‍💻 - **Not there yet, but soon!**
💤 - **I did not begin this part yet!**
- ✅ Support for Debian AND Ubuntu 20.04 LTS;
- ✅ Native support for SMTP relays (For example: SendGrid);
- ✅ Bumped the bootstrap and jQuery dependencies' versions - and we've got a brand new admin panel now!
- ✅ Per-domain `nginx` configuration: Custom pages will no longer have their pages defaulting to the MiaB services (`/admin`, `/mail`, etc.);
- ✅ Updated NextCloud to the latest version available;
- ✅ Performing backups immediately from the admin panel (independently from the daily schedule);
- 👨‍💻 Encrypting backups using user-provided PGP keys;
- 👨‍💻 Integrate a WKD server (Web Key Directory) for PGP keys;
- 💤 Restricting access to the admin panel to certain IP's?
- 💤 Customizing MTA names? (because privacy)
### Ideas section:
- 💤 Ability to download the backups from the admin panel;
- 💤 Possibility of making some services optional (if they require more software to be installed) on setup?
- - For example, one might simply not use NextCloud/Munin at all, and they're there... just wasting resources.
- 💤 AXFR Transfers (for secondary DNS) using TSIG?
- 💤 Expand DNS record options?
- 💤 More complete webmail configuration via the admin panel/plugin management?
- 💤 Expand the TOTP Two-Factor-Authentication for the webmail?
- - Maybe U2F one day, too, but I don't have a capable device for this just yet...
- 💤 Anything else I might need to use;
All in all, I think I should rename this to something like "Central [Clown Computing](https://www.urbandictionary.com/define.php?term=clown%20computing)", since I'm trying to cram as many services as possible into that poor machine (Spending 5$ is better than spending 10$)
Original Documentation
======================
Mail-in-a-Box
=============
By [@JoshData](https://github.com/JoshData) and [contributors](https://github.com/mail-in-a-box/mailinabox/graphs/contributors).
@ -96,7 +24,7 @@ Our goals are to:
* Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web.
* Have automated, auditable, and [idempotent](https://web.archive.org/web/20190518072631/https://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
* **Not** make a totally unhackable, NSA-proof server.
* ~~**Not** make something customizable by power users.~~
* **Not** make something customizable by power users.
Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which supersedes the goals above. Please review it when joining our community.
@ -104,7 +32,7 @@ Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which su
In The Box
----------
Mail-in-a-Box turns a fresh ~~Ubuntu 18.04 LTS~~ Debian 10 (Buster) 64-bit machine into a working mail server by installing and configuring various components.
Mail-in-a-Box turns a fresh Ubuntu 20.04 or 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components.
It is a one-click email appliance. There are no user-configurable setup options. It "just works."
@ -139,7 +67,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele
$ git clone https://github.com/mail-in-a-box/mailinabox
$ cd mailinabox
$ git checkout v0.52
$ git checkout v0.53
Begin the installation.

21
Vagrantfile vendored
View File

@ -1,20 +1,8 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "debian/buster64"
config.vm.provider :virtualbox do |vb|
vb.customize ["modifyvm", :id, "--cpus", 4, "--memory", 4096]
end
config.vm.provider :libvirt do |v|
v.memory = 4096
v.cpus = 4
v.nested = true
end
config.vm.provider :kvm do |kvm|
kvm.memory_size = '4096m'
end
config.vm.box = "ubuntu/bionic64"
# Network config: Since it's a mail server, the machine must be connected
# to the public web. However, we currently don't want to expose SSH since
@ -22,18 +10,17 @@ Vagrant.configure("2") do |config|
# machine on a private network.
config.vm.hostname = "mailinabox.lan"
config.vm.network "private_network", ip: "192.168.50.4"
config.vm.synced_folder ".", "/vagrant", nfs_version: "3"
#, :mount_options => ["ro"]
config.vm.provision :shell, :inline => <<-SH
# Set environment variables so that the setup script does
# not ask any questions during provisioning. We'll let the
# machine figure out its own public IP.
export NONINTERACTIVE=1
export PUBLIC_IP=192.168.50.4
export PUBLIC_IP=auto
export PUBLIC_IPV6=auto
export PRIMARY_HOSTNAME=auto
export SKIP_NETWORK_CHECKS=1
#export SKIP_NETWORK_CHECKS=1
# Start the setup script.
cd /vagrant
setup/start.sh

View File

@ -499,123 +499,6 @@ paths:
text/html:
schema:
type: string
/system/backup/new:
post:
tags:
- System
summary: Perform system backup
description: Performs a system backup.
operationId: performSystemBackup
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/PerformBackupRequest'
examples:
incremental:
summary: Perform incremental backup.
value:
full: false
full:
summary: Force a full backup.
value:
full: true
x-codeSamples:
- lang: curl
source: |
curl -X POST "https://{host}/admin/system/backup/new" \
-d "full=<boolean>" \
-u "<email>:<password>"
responses:
200:
description: Successful operation
content:
text/html:
schema:
$ref: '#/components/schemas/PerformBackupResponse'
403:
description: Forbidden
content:
text/plain:
schema:
type: string
/system/smtp/relay:
get:
tags:
- System
summary: Get SMTP relay configuration
description: Gets basic configuration on how the box should use third-party relay services to deliver mail.
operationId: getRelayConfig
x-codeSamples:
- lang: curl
source: |
curl -X GET "https://{host}/admin/system/smtp/relay" \
-u "<email>:<password>"
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/SmtpRelayConfig'
403:
description: Forbidden
content:
text/html:
schema:
type: string
post:
tags:
- System
summary: Set SMTP relay configuration
description: Sets the configuration on how the box should use third-party relays to deliver mail.
operationId: setRelayConfig
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/SetSmtpRelayConfigRequest'
examples:
disable:
summary: Do not use relays.
value:
enabled: false
host: ""
auth_enabled: false
user: ""
key: ""
no_auth:
summary: Use a relay that does not require authentication.
value:
enabled: true
host: smtp.relay.net
auth_enabled: false
user: ""
key: ""
auth:
summary: Use a relay that requires authentication.
value:
enabled: true
host: smtp.relay.net
auth_enabled: true
user: someuser
key: key-or-password-here
responses:
200:
description: Successful operation
content:
text/plain:
schema:
type: string
403:
description: Forbidden
content:
text/html:
schema:
type: string
/ssl/status:
get:
tags:
@ -2576,19 +2459,6 @@ components:
minimum: 1
example: 3
description: Backup config update request.
PerformBackupRequest:
type: object
required:
- full
properties:
full:
type: boolean
example: false
description: New backup type.
PerformBackupResponse:
type: string
example: OK
description: Backup creation response.
SystemBackupConfigUpdateResponse:
type: string
example: OK
@ -2791,52 +2661,6 @@ components:
type: string
example: web updated
description: Web update response.
SmtpRelayConfig:
type: object
required:
- enabled
- host
- auth_enabled
- user
properties:
enabled:
type: boolean
example: true
host:
type: string
example: sendgrid.net
auth_enabled:
type: boolean
example: true
user:
type: string
example: someuser
description: SMTP configuration details.
SetSmtpRelayConfigRequest:
type: object
required:
- enabled
- host
- auth_enabled
- user
- key
properties:
enabled:
type: boolean
example: true
host:
type: string
example: sendgrid.net
auth_enabled:
type: boolean
example: true
user:
type: string
example: apikey
key:
type: string
example: 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
description: SMTP Configuration form
MfaStatusResponse:
type: object
properties:

View File

@ -1,6 +1,6 @@
# Expose this directory as static files.
root $ROOT;
# ADDITIONAL DIRECTIVES HERE
index index.html index.htm;
location = /robots.txt {
log_not_found off;
@ -25,24 +25,31 @@
alias /var/lib/mailinabox/mta-sts.txt;
}
# Z-Push (Microsoft Exchange ActiveSync)
location /Microsoft-Server-ActiveSync {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_read_timeout 630;
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
rewrite ^/mail/$ /mail/index.php;
location /mail/ {
index index.php;
alias /usr/local/lib/roundcubemail/;
}
location ~ /mail/config/.* {
# A ~-style location is needed to give this precedence over the next block.
return 403;
}
location ~ /mail/.*\.php {
# note: ~ has precendence over a regular location block
include fastcgi_params;
fastcgi_split_path_info ^/mail(/.*)()$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
fastcgi_pass php-fpm;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
location ~* ^/autodiscover/autodiscover.xml$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_pass php-fpm;
}
# ADDITIONAL DIRECTIVES HERE
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
# This block is placed at the end. Nginx's precedence rules means this block

View File

@ -1,5 +1,3 @@
# ADDITIONAL DIRECTIVES HERE
# Control Panel
# Proxy /admin to our Python based control panel daemon. It is
# listening on IPv4 only so use an IP address and not 'localhost'.
@ -40,30 +38,6 @@
add_header Content-Security-Policy "frame-ancestors 'none';";
}
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
rewrite ^/mail/$ /mail/index.php;
location /mail/ {
index index.php;
alias /usr/local/lib/roundcubemail/;
}
location ~ /mail/config/.* {
# A ~-style location is needed to give this precedence over the next block.
return 403;
}
location ~ /mail/.*\.php {
# note: ~ has precendence over a regular location block
include fastcgi_params;
fastcgi_split_path_info ^/mail(/.*)()$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
fastcgi_pass php-fpm;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
# Nextcloud configuration.
rewrite ^/cloud$ /cloud/ redirect;
rewrite ^/cloud/$ /cloud/index.php;
@ -122,3 +96,5 @@
rewrite ^/.well-known/host-meta.json /cloud/public.php?service=host-meta-json last;
rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect;
rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect;
# ADDITIONAL DIRECTIVES HERE

View File

@ -1,15 +1,10 @@
<html>
<head>
<title>Redirecting...</title>
<meta name="robots" content="noindex">
</head>
<body>
</body>
<script>
location.href = "/mail"
</script>
<head>
<title>this is a mail-in-a-box</title>
<meta name="robots" content="noindex">
</head>
<body>
<h1>this is a mail-in-a-box</h1>
<p>take control of your email at <a href="https://mailinabox.email/">https://mailinabox.email/</a></p>
</body>
</html>

View File

@ -10,7 +10,7 @@
import os, os.path, shutil, glob, re, datetime, sys
import dateutil.parser, dateutil.relativedelta, dateutil.tz
import rtyaml
from exclusiveprocess import Lock, CannotAcquireLock
from exclusiveprocess import Lock
from utils import load_environment, shell, wait_for_service, fix_boto, get_php_version
@ -210,21 +210,13 @@ def get_target_type(config):
protocol = config["target"].split(":")[0]
return protocol
def perform_backup(full_backup, user_initiated=False):
def perform_backup(full_backup):
env = load_environment()
php_fpm = f"php{get_php_version()}-fpm"
# Create an global exclusive lock so that the backup script
# cannot be run more than one.
lock = Lock(name="mailinabox_backup_daemon", die=(not user_initiated))
if user_initiated:
# God forgive me for what I'm about to do
try:
lock._acquire()
except CannotAcquireLock:
return "Another backup is already being done!"
else:
lock.forever()
# cannot be run more than once.
Lock(die=True).forever()
config = get_backup_config(env)
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
@ -329,13 +321,8 @@ def perform_backup(full_backup, user_initiated=False):
# backup. Since it checks that dovecot and postfix are running, block for a
# bit (maximum of 10 seconds each) to give each a chance to finish restarting
# before the status checks might catch them down. See #381.
if user_initiated:
# God forgive me for what I'm about to do
lock._release()
# We don't need to wait for the services to be up in this case
else:
wait_for_service(25, True, env, 10)
wait_for_service(993, True, env, 10)
wait_for_service(25, True, env, 10)
wait_for_service(993, True, env, 10)
# Execute a post-backup script that does the copying to a remote server.
# Run as the STORAGE_USER user, not as root. Pass our settings in
@ -346,7 +333,6 @@ def perform_backup(full_backup, user_initiated=False):
['su', env['STORAGE_USER'], '-c', post_script, config["target"]],
env=env)
def run_duplicity_verification():
env = load_environment()
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')

View File

@ -280,17 +280,50 @@ def dns_set_secondary_nameserver():
@app.route('/dns/custom')
@authorized_personnel_only
def dns_get_records(qname=None, rtype=None):
from dns_update import get_custom_dns_config
return json_response([
{
"qname": r[0],
"rtype": r[1],
"value": r[2],
}
for r in get_custom_dns_config(env)
if r[0] != "_secondary_nameserver"
and (not qname or r[0] == qname)
and (not rtype or r[1] == rtype) ])
# Get the current set of custom DNS records.
from dns_update import get_custom_dns_config, get_dns_zones
records = get_custom_dns_config(env, only_real_records=True)
# Filter per the arguments for the more complex GET routes below.
records = [r for r in records
if (not qname or r[0] == qname)
and (not rtype or r[1] == rtype) ]
# Make a better data structure.
records = [
{
"qname": r[0],
"rtype": r[1],
"value": r[2],
"sort-order": { },
} for r in records ]
# To help with grouping by zone in qname sorting, label each record with which zone it is in.
# There's an inconsistency in how we handle zones in get_dns_zones and in sort_domains, so
# do this first before sorting the domains within the zones.
zones = utils.sort_domains([z[0] for z in get_dns_zones(env)], env)
for r in records:
for z in zones:
if r["qname"] == z or r["qname"].endswith("." + z):
r["zone"] = z
break
# Add sorting information. The 'created' order follows the order in the YAML file on disk,
# which tracs the order entries were added in the control panel since we append to the end.
# The 'qname' sort order sorts by our standard domain name sort (by zone then by qname),
# then by rtype, and last by the original order in the YAML file (since sorting by value
# may not make sense, unless we parse IP addresses, for example).
for i, r in enumerate(records):
r["sort-order"]["created"] = i
domain_sort_order = utils.sort_domains([r["qname"] for r in records], env)
for i, r in enumerate(sorted(records, key = lambda r : (
zones.index(r["zone"]),
domain_sort_order.index(r["qname"]),
r["rtype"]))):
r["sort-order"]["qname"] = i
# Return.
return json_response(records)
@app.route('/dns/custom/<qname>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@app.route('/dns/custom/<qname>/<rtype>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@ -594,19 +627,6 @@ def backup_set_custom():
request.form.get('min_age', '')
))
@app.route('/system/backup/new', methods=["POST"])
@authorized_personnel_only
def backup_new():
from backup import perform_backup, get_backup_config
# If backups are disabled, don't perform the backup
config = get_backup_config(env)
if config["target"] == "off":
return "Backups are disabled in this machine. Nothing was done."
msg = perform_backup(request.form.get('full', False) == 'true', True)
return "OK" if msg is None else msg
@app.route('/system/privacy', methods=["GET"])
@authorized_personnel_only
def privacy_status_get():
@ -621,49 +641,6 @@ def privacy_status_set():
utils.write_settings(config, env)
return "OK"
@app.route('/system/smtp/relay', methods=["GET"])
@authorized_personnel_only
def smtp_relay_get():
config = utils.load_settings(env)
return {
"enabled": config.get("SMTP_RELAY_ENABLED", True),
"host": config.get("SMTP_RELAY_HOST", ""),
"auth_enabled": config.get("SMTP_RELAY_AUTH", False),
"user": config.get("SMTP_RELAY_USER", "")
}
@app.route('/system/smtp/relay', methods=["POST"])
@authorized_personnel_only
def smtp_relay_set():
from editconf import edit_conf
config = utils.load_settings(env)
newconf = request.form
try:
# Write on daemon settings
config["SMTP_RELAY_ENABLED"] = (newconf.get("enabled") == "true")
config["SMTP_RELAY_HOST"] = newconf.get("host")
config["SMTP_RELAY_AUTH"] = (newconf.get("auth_enabled") == "true")
config["SMTP_RELAY_USER"] = newconf.get("user")
utils.write_settings(config, env)
# Write on Postfix configs
edit_conf("/etc/postfix/main.cf", [
"relayhost=" + (f"[{config['SMTP_RELAY_HOST']}]:587" if config["SMTP_RELAY_ENABLED"] else ""),
"smtp_sasl_auth_enable=" + ("yes" if config["SMTP_RELAY_AUTH"] else "no"),
"smtp_sasl_security_options=" + ("noanonymous" if config["SMTP_RELAY_AUTH"] else "anonymous"),
"smtp_sasl_tls_security_options=" + ("noanonymous" if config["SMTP_RELAY_AUTH"] else "anonymous")
], delimiter_re=r"\s*=\s*", delimiter="=", comment_char="#")
if config["SMTP_RELAY_AUTH"]:
# Edit the sasl password
with open("/etc/postfix/sasl_passwd", "w") as f:
f.write(f"[{config['SMTP_RELAY_HOST']}]:587 {config['SMTP_RELAY_USER']}:{newconf.get('key')}\n")
utils.shell("check_output", ["/usr/bin/chmod", "600", "/etc/postfix/sasl_passwd"], capture_stderr=True)
utils.shell("check_output", ["/usr/sbin/postmap", "/etc/postfix/sasl_passwd"], capture_stderr=True)
# Restart Postfix
return utils.shell("check_output", ["/usr/bin/systemctl", "restart", "postfix"], capture_stderr=True)
except Exception as e:
return (str(e), 500)
# MUNIN
@app.route('/munin/')

View File

@ -12,14 +12,14 @@ export LC_TYPE=en_US.UTF-8
# On Mondays, i.e. once a week, send the administrator a report of total emails
# sent and received so the admin might notice server abuse.
if [ `date "+%u"` -eq 1 ]; then
management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report"
management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report"
fi
# Take a backup.
management/backup.py 2>&1 | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
# Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"

View File

@ -753,7 +753,7 @@ def write_opendkim_tables(domains, env):
########################################################################
def get_custom_dns_config(env):
def get_custom_dns_config(env, only_real_records=False):
try:
custom_dns = rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')))
if not isinstance(custom_dns, dict): raise ValueError() # caught below
@ -761,6 +761,8 @@ def get_custom_dns_config(env):
return [ ]
for qname, value in custom_dns.items():
if qname == "_secondary_nameserver" and only_real_records: continue # skip fake record
# Short form. Mapping a domain name to a string is short-hand
# for creating A records.
if isinstance(value, str):

View File

@ -44,9 +44,8 @@ TIME_DELTAS = OrderedDict([
('today', datetime.datetime.now() - datetime.datetime.now().replace(hour=0, minute=0, second=0))
])
# Start date > end date!
START_DATE = datetime.datetime.now()
END_DATE = None
END_DATE = NOW = datetime.datetime.now()
START_DATE = None
VERBOSE = False
@ -121,7 +120,7 @@ def scan_mail_log(env):
pass
print("Scanning logs from {:%Y-%m-%d %H:%M:%S} to {:%Y-%m-%d %H:%M:%S}".format(
END_DATE, START_DATE)
START_DATE, END_DATE)
)
# Scan the lines in the log files until the date goes out of range
@ -253,7 +252,7 @@ def scan_mail_log(env):
if collector["postgrey"]:
msg = "Greylisted Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
print_header(msg.format(END_DATE, START_DATE))
print_header(msg.format(START_DATE, END_DATE))
print(textwrap.fill(
"The following mail was greylisted, meaning the emails were temporarily rejected. "
@ -291,7 +290,7 @@ def scan_mail_log(env):
if collector["rejected"]:
msg = "Blocked Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
print_header(msg.format(END_DATE, START_DATE))
print_header(msg.format(START_DATE, END_DATE))
data = OrderedDict(sorted(collector["rejected"].items(), key=email_sort))
@ -345,19 +344,19 @@ def scan_mail_log_line(line, collector):
# Replaced the dateutil parser for a less clever way of parser that is roughly 4 times faster.
# date = dateutil.parser.parse(date)
# date = datetime.datetime.strptime(date, '%b %d %H:%M:%S')
# date = date.replace(START_DATE.year)
# strptime fails on Feb 29 if correct year is not provided. See https://bugs.python.org/issue26460
date = datetime.datetime.strptime(str(START_DATE.year) + ' ' + date, '%Y %b %d %H:%M:%S')
# print("date:", date)
# strptime fails on Feb 29 with ValueError: day is out of range for month if correct year is not provided.
# See https://bugs.python.org/issue26460
date = datetime.datetime.strptime(str(NOW.year) + ' ' + date, '%Y %b %d %H:%M:%S')
# if log date in future, step back a year
if date > NOW:
date = date.replace(year = NOW.year - 1)
#print("date:", date)
# Check if the found date is within the time span we are scanning
# END_DATE < START_DATE
if date > START_DATE:
if date > END_DATE:
# Don't process, and halt
return False
elif date < END_DATE:
elif date < START_DATE:
# Don't process, but continue
return True
@ -606,7 +605,7 @@ def email_sort(email):
def valid_date(string):
""" Validate the given date string fetched from the --startdate argument """
""" Validate the given date string fetched from the --enddate argument """
try:
date = dateutil.parser.parse(string)
except ValueError:
@ -820,12 +819,14 @@ if __name__ == "__main__":
parser.add_argument("-t", "--timespan", choices=TIME_DELTAS.keys(), default='today',
metavar='<time span>',
help="Time span to scan, going back from the start date. Possible values: "
help="Time span to scan, going back from the end date. Possible values: "
"{}. Defaults to 'today'.".format(", ".join(list(TIME_DELTAS.keys()))))
parser.add_argument("-d", "--startdate", action="store", dest="startdate",
type=valid_date, metavar='<start date>',
help="Date and time to start scanning the log file from. If no date is "
"provided, scanning will start from the current date and time.")
# keep the --startdate arg for backward compatibility
parser.add_argument("-d", "--enddate", "--startdate", action="store", dest="enddate",
type=valid_date, metavar='<end date>',
help="Date and time to end scanning the log file. If no date is "
"provided, scanning will end at the current date and time. "
"Alias --startdate is for compatibility.")
parser.add_argument("-u", "--users", action="store", dest="users",
metavar='<email1,email2,email...>',
help="Comma separated list of (partial) email addresses to filter the "
@ -837,13 +838,13 @@ if __name__ == "__main__":
args = parser.parse_args()
if args.startdate is not None:
START_DATE = args.startdate
if args.enddate is not None:
END_DATE = args.enddate
if args.timespan == 'today':
args.timespan = 'day'
print("Setting start date to {}".format(START_DATE))
print("Setting end date to {}".format(END_DATE))
END_DATE = START_DATE - TIME_DELTAS[args.timespan]
START_DATE = END_DATE - TIME_DELTAS[args.timespan]
VERBOSE = args.verbose

View File

@ -280,9 +280,9 @@ def run_network_checks(env, output):
if ret == 0:
output.print_ok("Outbound mail (SMTP port 25) is not blocked.")
else:
output.print_warning("""Outbound mail (SMTP port 25) seems to be blocked by your network. You
will not be able to send any mail without a SMTP relay. Many residential networks block port 25 to prevent
hijacked machines from being able to send spam. A quick connection test to Google's mail server on port 25
output.print_error("""Outbound mail (SMTP port 25) seems to be blocked by your network. You
will not be able to send any mail. Many residential networks block port 25 to prevent hijacked
machines from being able to send spam. A quick connection test to Google's mail server on port 25
failed.""")
# Stop if the IPv4 address is listed in the ZEN Spamhaus Block List.
@ -300,19 +300,6 @@ def run_network_checks(env, output):
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
% (env['PUBLIC_IP'], zen, env['PUBLIC_IP']))
# Check if a SMTP relay is set up. It's not strictly required, but on some providers
# it might be needed.
config = load_settings(env)
if config.get("SMTP_RELAY_ENABLED"):
if config.get("SMTP_RELAY_AUTH"):
output.print_ok("An authenticated SMTP relay has been set up via port 587.")
else:
output.print_warning("A SMTP relay has been set up, but it is not authenticated.")
elif ret == 0:
output.print_ok("No SMTP relay has been set up (but that's ok since port 25 is not blocked).")
else:
output.print_error("No SMTP relay has been set up. Since port 25 is blocked, you will probably not be able to send any mail.")
def run_domain_checks(rounded_time, env, output, pool):
# Get the list of domains we handle mail for.
mail_domains = get_mail_domains(env)

View File

@ -94,10 +94,10 @@
<tr id="alias-template">
<td class='actions'>
<a href="#" onclick="aliases_edit(this); scroll_top(); return false;" class='edit' title="Edit Alias">
<span class="fas fa-pen"></span>
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="#" onclick="aliases_remove(this); return false;" class='remove' title="Remove Alias">
<span class="fas fa-trash"></span>
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
<td class='address'> </td>
@ -153,8 +153,8 @@ function show_aliases() {
function(r) {
$('#alias_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><td colspan='3'><h4/></td></tr>");
hdr.find('h4').text(r[i].domain);
var hdr = $("<tr><th colspan='4' style='background-color: #EEE'></th></tr>");
hdr.find('th').text(r[i].domain);
$('#alias_table tbody').append(hdr);
for (var k = 0; k < r[i].aliases.length; k++) {

View File

@ -57,7 +57,13 @@
</div>
</form>
<table id="custom-dns-current" class="table" style="width: auto; display: none">
<div style="text-align: right; font-size; 90%; margin-top: 1em;">
sort by:
<a href="#" onclick="window.miab_custom_dns_data_sort_order='qname'; show_current_custom_dns_update_after_sort(); return false;">domain name</a>
|
<a href="#" onclick="window.miab_custom_dns_data_sort_order='created'; show_current_custom_dns_update_after_sort(); return false;">created</a>
</div>
<table id="custom-dns-current" class="table" style="width: auto; display: none; margin-top: 0;">
<thead>
<th>Domain Name</th>
<th>Record Type</th>
@ -192,36 +198,38 @@ function show_current_custom_dns() {
$('#custom-dns-current').fadeIn();
else
$('#custom-dns-current').fadeOut();
window.miab_custom_dns_data = data;
show_current_custom_dns_update_after_sort();
});
}
var reverse_fqdn = function(el) {
el.qname = el.qname.split('.').reverse().join('.');
return el;
}
var sort = function(a, b) {
if(a.qname === b.qname) {
if(a.rtype === b.rtype) {
return a.value > b.value ? 1 : -1;
}
return a.rtype > b.rtype ? 1 : -1;
}
return a.qname > b.qname ? 1 : -1;
}
function show_current_custom_dns_update_after_sort() {
var data = window.miab_custom_dns_data;
var sort_key = window.miab_custom_dns_data_sort_order || "qname";
data = data.map(reverse_fqdn).sort(sort).map(reverse_fqdn);
data.sort(function(a, b) { return a["sort-order"][sort_key] - b["sort-order"][sort_key] });
$('#custom-dns-current').find("tbody").text('');
var tbody = $('#custom-dns-current').find("tbody");
tbody.text('');
var last_zone = null;
for (var i = 0; i < data.length; i++) {
if (sort_key == "qname" && data[i].zone != last_zone) {
var r = $("<tr><th colspan=4 style='background-color: #EEE'></th></tr>");
r.find("th").text(data[i].zone);
tbody.append(r);
last_zone = data[i].zone;
}
var tr = $("<tr/>");
$('#custom-dns-current').find("tbody").append(tr);
tbody.append(tr);
tr.attr('data-qname', data[i].qname);
tr.attr('data-rtype', data[i].rtype);
tr.attr('data-value', data[i].value);
tr.append($('<td class="long"/>').text(data[i].qname));
tr.append($('<td/>').text(data[i].rtype));
tr.append($('<td class="long"/>').text(data[i].value));
tr.append($('<td class="long" style="max-width: 40em"/>').text(data[i].value));
tr.append($('<td>[<a href="#" onclick="return delete_custom_dns_record(this)">delete</a>]</td>'));
}
});
}
function delete_custom_dns_record(elem) {

View File

@ -36,7 +36,7 @@
<p class="alert" role="alert">
<span class="fas fa-info-circle"></span>
<span class="glyphicon glyphicon-info-sign"></span>
You may encounter zone file errors when attempting to create a TXT record with a long string.
<a href="http://tools.ietf.org/html/rfc4408#section-3.1.3">RFC 4408</a> states a TXT record is allowed to contain multiple strings, and this technique can be used to construct records that would exceed the 255-byte maximum length.
You may need to adopt this technique when adding DomainKeys. Use a tool like <code>named-checkzone</code> to validate your zone file.
@ -50,7 +50,7 @@
<label for="downloadZonefile" class="control-label sr-only">Zone</label>
<select id="downloadZonefile" class="form-control" style="width: auto"> </select>
</div>
<button type="submit" style="margin-left: 20px" class="btn btn-primary">Download</button>
<button type="submit" class="btn btn-primary">Download</button>
</div>
</form>

View File

@ -9,10 +9,7 @@
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/assets/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="/admin/assets/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
<style>
body {
overflow-y: scroll;
@ -66,6 +63,7 @@
margin-bottom: 1em;
}
</style>
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap-theme.min.css">
</head>
<body>
@ -73,52 +71,53 @@
<!--[if gt IE 7]><!-->
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container bg-light">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{hostname}}</a>
</div>
<div class="navbar navbar-expand-lg">
<div class="navbar-collapse collapse">
<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>
<li class="dropdown">
<a 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><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
<li><a href="#tls" onclick="return show_panel(this);">TLS (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">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>
<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>
<li><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>
<li class="dropdown">
<a 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><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>
<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>
<li><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>
<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="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 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 id="panel_smtp_relays" class="admin_panel">
{% include "smtp-relays.html" %}
</div>
<div id="panel_system_status" class="admin_panel">
{% include "system-status.html" %}
</div>
@ -170,7 +169,7 @@
<hr>
<footer>
<p>This is a <a href="https://github.com/ddavness/power-mailinabox">Power Mail-in-a-Box</a> - {{distname}}</p>
<p>This is a <a href="https://mailinabox.email">Mail-in-a-Box</a>.</p>
</footer>
</div> <!-- /container -->
@ -185,8 +184,8 @@
<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>
<h4 class="modal-title" id="errorModalTitle"> </h4>
</div>
<div class="modal-body">
<p> </p>

View File

@ -51,19 +51,19 @@ sudo management/cli.py user make-admin me@{{hostname}}</pre>
<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">
<div class="col-sm-9">
<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">
<div class="col-sm-9">
<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">
<div class="col-sm-9">
<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>

View File

@ -36,11 +36,11 @@
<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">
<div class="panel panel-danger">
<div class="panel-heading">
Enabling two-factor authentication does not protect access to your email
</div>
<div class="card-body bg-light">
<div class="panel-body">
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
@ -81,7 +81,7 @@ and ensure every administrator account for this control panel does the same.</st
<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>
<button id="totp-setup-submit" disabled type="submit" class="btn">Enable Two-Factor Authentication</button>
</div>
</form>
@ -95,8 +95,8 @@ and ensure every administrator account for this control panel does the same.</st
</div>
</form>
<div id="output-2fa" class="card bg-light">
<div class="card-body"></div>
<div id="output-2fa" class="panel panel-danger">
<div class="panel-body"></div>
</div>
</div>
@ -155,12 +155,12 @@ and ensure every administrator account for this control panel does the same.</st
}
function hide_error() {
el.output.querySelector('.card-body').innerHTML = '';
el.output.querySelector('.panel-body').innerHTML = '';
el.output.classList.remove('visible');
}
function render_error(msg) {
el.output.querySelector('.card-body').innerHTML = msg;
el.output.querySelector('.panel-body').innerHTML = msg;
el.output.classList.add('visible');
}

View File

@ -1,135 +0,0 @@
<style>
</style>
<h2>SMTP Relays</h2>
<p>SMTP Relays are third-party services you can hand off the responsability of getting the mail delivered. They
can be useful when, for example, port 25 is blocked.</p>
<p>Here, you can configure an authenticated SMTP relay (for example, <a href="https://sendgrid.com/"
target="_blank">SendGrid</a>) over port 587.</p>
<div id="smtp_relay_config">
<h3>SMTP Relay Configuration</h3>
<form class="form-horizontal" role="form" onsubmit="set_smtp_relay_config(); return false;">
<div class="form-group">
<table id="smtp-relays" class="table" style="width: 600px">
<tr>
<td>
<label for="use_relay" class="col-sm-1 control-label">Use Relay?</label>
</td>
<td>
<div class="col-sm-10">
<input type="checkbox" id="use_relay" name="use_relay" value="true"
onclick="checkfields();">
</div>
</td>
</tr>
<tr>
<td>
<label for="relay_host" class="col-sm-1 control-label">Hostname</label>
</td>
<td>
<div class="col-sm-10">
<input type="text" class="form-control" id="relay_host" placeholder="host.domain.tld">
</div>
</td>
<td style="padding: 0; font-weight: bold;">:587</td>
</tr>
<tr>
<td>
<label for="relay_use_auth" class="col-sm-1 control-label">Authenticate</label>
</td>
<td>
<div class="col-sm-10">
<input checked type="checkbox" id="relay_use_auth" name="relay_use_auth" value="true"
onclick="checkfields();">
</div>
</td>
</tr>
<tr>
<td>
<label for="relay_auth_user" class="col-sm-1 control-label">Username</label>
</td>
<td>
<div class="col-sm-10">
<input type="text" class="form-control" id="relay_auth_user" placeholder="user">
</div>
</td>
</tr>
<tr>
<td>
<label for="relay_auth_pass" class="col-sm-1 control-label">Password/Key</label>
</td>
<td>
<div class="col-sm-10">
<input type="password" class="form-control" id="relay_auth_pass" placeholder="password">
</div>
</td>
</tr>
</table>
</div>
<div>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
<script>
const use_relay = document.getElementById("use_relay")
const relay_host = document.getElementById("relay_host")
const relay_use_auth = document.getElementById("relay_use_auth")
const relay_auth_user = document.getElementById("relay_auth_user")
const relay_auth_pass = document.getElementById("relay_auth_pass")
function checkfields() {
let relay_enabled = use_relay.checked
let auth_enabled = relay_use_auth.checked
relay_host.disabled = !relay_enabled
relay_use_auth.disabled = !relay_enabled
relay_auth_user.disabled = !(relay_enabled && auth_enabled)
relay_auth_pass.disabled = !(relay_enabled && auth_enabled)
}
function show_smtp_relays() {
api(
"/system/smtp/relay",
"GET",
{},
data => {
use_relay.checked = data.enabled
relay_host.value = data.host
relay_use_auth.checked = data.auth_enabled
relay_auth_user.value = data.user
relay_auth_pass.value = ""
checkfields()
}
)
}
function set_smtp_relay_config() {
api(
"/system/smtp/relay",
"POST",
{
enabled: use_relay.checked,
host: relay_host.value,
auth_enabled: relay_use_auth.checked,
user: relay_auth_user.value,
key: relay_auth_pass.value
},
() => {
show_modal_error("Done!", "The configuration has been updated and Postfix was restarted successfully. Please make sure everything is functioning as intended.", () => {
return false
})
}
)
}
</script>

View File

@ -12,7 +12,6 @@
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
<button onclick='return provision_tls_cert();' class='btn btn-primary' style="float: left; margin: 0 1.5em 1em 0;">Provision</button>
<p><b>By provisioning the certificates, you&rsquo;re agreeing to the <a href="https://acme-v01.api.letsencrypt.org/terms">Let&rsquo;s Encrypt Subscriber Agreement</a>.</b></p>
<p>A TLS certificate can be automatically provisioned from <a href="https://letsencrypt.org/" target="_blank">Let&rsquo;s Encrypt</a>, a free TLS certificate provider, for:<br>
<span class="text-primary"></span></p>
</div>

View File

@ -12,12 +12,13 @@
<form class="form-horizontal" role="form" onsubmit="set_custom_backup(); return false;">
<div class="form-group">
<label for="backup-target-type" class="col-sm-2 control-label">Backup to:</label>
<div class="col-sm-3">
<div class="col-sm-2">
<select class="form-control" rows="1" id="backup-target-type" onchange="toggle_form()">
<option value="off">Nowhere (Disable Backups)</option>
<option value="local">{{hostname}}</option>
<option value="rsync">rsync</option>
<option value="s3">Amazon S3</option>
<option value="b2">Backblaze B2</option>
</select>
</div>
</div>
@ -165,11 +166,6 @@
<tbody>
</tbody>
</table>
<!-- Hide these buttons until we're sure we can use them :) -->
<button id="create-full-backup-button" class="btn btn-primary" onclick="do_backup(true)" style="display: none;">Create Full Backup Now</button>
<button id="create-incremental-backup-button" class="btn btn-primary" onclick="do_backup(false)" style="display: none;">Create Incremental Backup Now</button>
<script>
function toggle_form() {
@ -216,17 +212,12 @@ function show_system_backup() {
if (typeof r.backups == "undefined") {
var tr = $('<tr><td colspan="3">Backups are turned off.</td></tr>');
$('#backup-status tbody').append(tr);
$('#create-full-backup-button').css("display","none")
$('#create-incremental-backup-button').css("display","none")
return;
} else if (r.backups.length == 0) {
var tr = $('<tr><td colspan="3">No backups have been made yet.</td></tr>');
$('#backup-status tbody').append(tr);
}
// Backups ARE enabled.
$('#create-full-backup-button').css("display","unset")
$('#create-incremental-backup-button').css("display","unset")
for (var i = 0; i < r.backups.length; i++) {
var b = r.backups[i];
var tr = $('<tr/>');
@ -352,29 +343,4 @@ function init_inputs(target_type) {
set_host($('#backup-target-s3-host-select').val());
}
}
function do_backup(is_full) {
let disclaimer = "The backup process will pause some services (such as PHP, Postfix and Dovecot). Depending on the size of the data this can take a while."
if (!is_full) {
disclaimer += "\nDepending on the amount of incremental backups done after the last full backup, the box may decide to do a full backup instead."
}
show_modal_confirm("Warning!", disclaimer, "Start Backup", () => {
api(
"/system/backup/new",
"POST",
{
full: is_full
},
function(r) {
// use .text() --- it's a text response, not html
show_modal_error("Backup configuration", $("<p/>").text(r), function() { if (r == "OK") show_system_backup(); }); // refresh after modal on success
},
function(r) {
// use .text() --- it's a text response, not html
show_modal_error("Backup configuration", $("<p/>").text(r));
});
return false;
})
}
</script>

View File

@ -1,168 +1,160 @@
<h2>System Status Checks</h2>
<style>
#system-checks .heading td {
font-weight: bold;
font-size: 120%;
padding-top: 1.5em;
}
#system-checks .heading.first td {
border-top: none;
padding-top: 0;
}
#system-checks .status-error td {
color: rgb(140, 0, 0);
}
#system-checks .status-warning td {
color: rgb(170, 120, 0);
}
#system-checks .status-ok td {
color: rgb(0, 140, 0);
}
#system-checks div.extra {
display: none;
margin-top: 1em;
max-width: 50em;
word-wrap: break-word;
}
#system-checks .showhide {
display: none;
font-size: 85%;
}
#system-checks .pre {
margin: 1em;
font-family: monospace;
white-space: pre-wrap;
}
#system-checks .heading td {
font-weight: bold;
font-size: 120%;
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>
<div>
<div>
<div class="row">
<div class="col-md-push-9 col-md-3">
<div id="system-reboot-required" style="display: none; margin-bottom: 1em;">
<button type="button" class="btn btn-danger" onclick="confirm_reboot(); return false;">Reboot Box</button>
<div>No reboot is necessary.</div>
</div>
<div id="system-reboot-required" style="display: none; margin-bottom: 1em;">
<button type="button" class="btn btn-danger" onclick="confirm_reboot(); return false;">Reboot Box</button>
<div>No reboot is necessary.</div>
</div>
<div id="system-privacy-setting" style="display: none">
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span>
New-Version Check</a></div>
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of
Mail-in-a-Box.)</small></p>
</div>
<div id="system-privacy-setting" style="display: none">
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span> New-Version Check</a></div>
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of Mail-in-a-Box.)</small></p>
</div>
</div> <!-- /col -->
<br>
<div>
<div class="col-md-pull-3 col-md-8">
<table id="system-checks" class="table">
<thead></thead>
<tbody></tbody>
</table>
<table id="system-checks" class="table" style="max-width: 60em">
<thead>
</thead>
<tbody>
</tbody>
</table>
</div> <!-- /col -->
</div> <!-- /row -->
<script>
function show_system_status() {
$('#system-checks tbody').html("<tr><td class='text-muted'>Loading...</td></tr>")
function show_system_status() {
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/privacy",
"GET",
{},
function (r) {
current_privacy_setting = r;
$('#system-privacy-setting').show();
$('#system-privacy-setting a span').text(r ? "Enable" : "Disable");
$('#system-privacy-setting p').toggle(r);
});
api(
"/system/privacy",
"GET",
{ },
function(r) {
current_privacy_setting = r;
$('#system-privacy-setting').show();
$('#system-privacy-setting a span').text(r ? "Enable" : "Disable");
$('#system-privacy-setting p').toggle(r);
});
api(
"/system/reboot",
"GET",
{},
function (r) {
$('#system-reboot-required').show(); // show when r becomes available
$('#system-reboot-required').find('button').toggle(r);
$('#system-reboot-required').find('div').toggle(!r);
});
api(
"/system/reboot",
"GET",
{ },
function(r) {
$('#system-reboot-required').show(); // show when r becomes available
$('#system-reboot-required').find('button').toggle(r);
$('#system-reboot-required').find('div').toggle(!r);
});
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'/><p class='showhide btn btn-light' href='#'/><div class='extra'></div></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);
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('.showhide').show().text("Show More").click(function () {
$(this).hide();
$(this).parent().find('.extra').fadeIn();
return false;
});
}
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++) {
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);
}
}
})
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);
}
}
})
}
}
var current_privacy_setting = null;
function enable_privacy(status) {
api(
"/system/privacy",
"POST",
{
value: (status ? "private" : "off")
},
function (res) {
show_system_status();
});
return false; // disable link
}
var current_privacy_setting = null;
function enable_privacy(status) {
api(
"/system/privacy",
"POST",
{
value: (status ? "private" : "off")
},
function(res) {
show_system_status();
});
return false; // disable link
}
function confirm_reboot() {
show_modal_confirm(
"Reboot",
$("<p>This will reboot your Mail-in-a-Box <code>{{hostname}}</code>.</p> <p>Until the machine is fully restarted, your users will not be able to send and receive email, and you will not be able to connect to this control panel or with SSH. The reboot cannot be cancelled.</p>"),
"Reboot Now",
function () {
api(
"/system/reboot",
"POST",
{},
function (r) {
var msg = "<p>Please reload this page after a minute or so.</p>";
if (r) msg = "<p>The reboot command said:</p> <pre>" + $("<pre/>").text(r).html() + "</pre>"; // successful reboots don't produce any output; the output must be HTML-escaped
show_modal_error("Reboot", msg);
});
});
}
function confirm_reboot() {
show_modal_confirm(
"Reboot",
$("<p>This will reboot your Mail-in-a-Box <code>{{hostname}}</code>.</p> <p>Until the machine is fully restarted, your users will not be able to send and receive email, and you will not be able to connect to this control panel or with SSH. The reboot cannot be cancelled.</p>"),
"Reboot Now",
function() {
api(
"/system/reboot",
"POST",
{ },
function(r) {
var msg = "<p>Please reload this page after a minute or so.</p>";
if (r) msg = "<p>The reboot command said:</p> <pre>" + $("<pre/>").text(r).html() + "</pre>"; // successful reboots don't produce any output; the output must be HTML-escaped
show_modal_error("Reboot", msg);
});
});
}
</script>

View File

@ -1,7 +1,6 @@
<h2>Users</h2>
<style>
#user_table h4 { margin: 1em 0 0 0; }
#user_table tr.account_inactive td.address { color: #888; text-decoration: line-through; }
#user_table .actions { margin-top: .33em; font-size: 95%; }
#user_table .account_inactive .if_active { display: none; }
@ -134,8 +133,8 @@ function show_users() {
function(r) {
$('#user_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><td colspan='3'><h4/></td></tr>");
hdr.find('h4').text(r[i].domain);
var hdr = $("<tr><th colspan='2' style='background-color: #EEE'></th></tr>");
hdr.find('th').text(r[i].domain);
$('#user_table tbody').append(hdr);
for (var k = 0; k < r[i].users.length; k++) {

View File

@ -206,16 +206,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
# Add in any user customizations in the includes/ folder.
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
if not os.path.exists(nginx_conf_custom_include):
with open(nginx_conf_custom_include, "a+") as f:
f.writelines([
f"# Custom configurations for {domain} go here\n",
"# To use php: use the \"php-fpm\" alias\n\n",
"index index.html index.htm;\n"
])
nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
if os.path.exists(nginx_conf_custom_include):
nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
# PUT IT ALL TOGETHER
# Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder

View File

@ -2,17 +2,43 @@
#########################################################
# This script is intended to be run like this:
#
# curl https://dvn.pt/power-miab | sudo bash
# curl https://mailinabox.email/setup.sh | sudo bash
#
#########################################################
if [ -z "$TAG" ]; then
# Make s
OS=`lsb_release -d | sed 's/.*:\s*//'`
if [ "$OS" == "Debian GNU/Linux 10 (buster)" -o "$(echo $OS | grep -o 'Ubuntu 20.04')" == "Ubuntu 20.04" ]; then
TAG=v0.52.POWER.0
# If a version to install isn't explicitly given as an environment
# variable, then install the latest version. But the latest version
# depends on the operating system. Existing Ubuntu 14.04 users need
# to be able to upgrade to the latest version supporting Ubuntu 14.04,
# in part because an upgrade is required before jumping to Ubuntu 18.04.
# New users on Ubuntu 18.04 need to get the latest version number too.
#
# Also, the system status checks read this script for TAG = (without the
# space, but if we put it in a comment it would confuse the status checks!)
# to get the latest version, so the first such line must be the one that we
# want to display in status checks.
if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/20\.04\.[0-9]/20.04/' `" == "Ubuntu 20.04 LTS" ]; then
# This machine is running Ubuntu 20.04.
TAG=v0.53
elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04.
TAG=v0.53
elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" == "Ubuntu 14.04 LTS" ]; then
# This machine is running Ubuntu 14.04.
echo "You are installing the last version of Mail-in-a-Box that will"
echo "support Ubuntu 14.04. If this is a new installation of Mail-in-a-Box,"
echo "stop now and switch to a machine running Ubuntu 18.04. If you are"
echo "upgrading an existing Mail-in-a-Box --- great. After upgrading this"
echo "box, please visit https://mailinabox.email for notes on how to upgrade"
echo "to Ubuntu 18.04."
echo ""
TAG=v0.30
else
echo "This script must be run on a system running Debian 10 OR Ubuntu 20.04 LTS."
echo "This script must be run on a system running Ubuntu 20.04, 18.04 or 14.04."
exit 1
fi
fi

View File

@ -62,7 +62,8 @@ chmod go-rwx $STORAGE_ROOT/mail/dkim
tools/editconf.py /etc/opendmarc.conf -s \
"Syslog=true" \
"Socket=inet:8893@[127.0.0.1]"
"Socket=inet:8893@[127.0.0.1]" \
"FailureReports=true"
# SPFIgnoreResults causes the filter to ignore any SPF results in the header
# of the message. This is useful if you want the filter to perfrom SPF checks
@ -81,6 +82,12 @@ tools/editconf.py /etc/opendmarc.conf -s \
tools/editconf.py /etc/opendmarc.conf -s \
"SPFSelfValidate=true"
# Enables generation of failure reports for sending domains that publish a
# "none" policy.
tools/editconf.py /etc/opendmarc.conf -s \
"FailureReportsOnNone=true"
# AlwaysAddARHeader Adds an "Authentication-Results:" header field even to
# unsigned messages from domains with no "signs all" policy. The reported DKIM
# result will be "none" in such cases. Normally unsigned mail from non-strict

View File

@ -1,3 +1,4 @@
#!/bin/bash
source setup/functions.sh
echo Installing geoip packages...
@ -8,8 +9,8 @@ echo Installing geoip packages...
gunzip -c tools/goiplookup.gz > /usr/local/bin/goiplookup
chmod +x /usr/local/bin/goiplookup
# check that geoipdb is older then 2 months, to not hit the server too often
if [[ ! -d /usr/share/GeoIP || ! -f /usr/share/GeoIP/GeoIP.dat || $(find "/usr/share/GeoIP/GeoIP.dat" -mtime +60 -print) ]]; then
# check that GeoLite2-Country.mmdb is older then 2 months, to not hit the server too often
if [[ ! -d /usr/share/GeoIP || ! -f /usr/share/GeoIP/GeoLite2-Country.mmdb || $(find "/usr/share/GeoIP/GeoLite2-Country.mmdb" -mtime +60 -print) ]]; then
echo updating goiplookup database
goiplookup db-update
else
@ -48,47 +49,56 @@ fi
## Install geo ip lookup files
# Move old file away if it exists
if [ -f "/usr/share/GeoIP/GeoIP.dat" ]; then
# check that GeoIP.dat is older then 2 months, to not hit the server too often
if [[ ! -d /usr/share/GeoIP || ! -f /usr/share/GeoIP/GeoIP.dat || $(find "/usr/share/GeoIP/GeoIP.dat" -mtime +60 -print) ]]; then
echo updating GeoIP database
# Move old file away if it exists
if [ -f "/usr/share/GeoIP/GeoIP.dat" ]; then
mv -f /usr/share/GeoIP/GeoIP.dat /usr/share/GeoIP/GeoIP.dat.bak
fi
fi
hide_output wget -P /usr/share/GeoIP/ https://dl.miyuru.lk/geoip/maxmind/country/maxmind.dat.gz
hide_output wget -P /usr/share/GeoIP/ https://dl.miyuru.lk/geoip/maxmind/country/maxmind.dat.gz
if [ -f "/usr/share/GeoIP/maxmind.dat.gz" ]; then
if [ -f "/usr/share/GeoIP/maxmind.dat.gz" ]; then
gunzip -c /usr/share/GeoIP/maxmind.dat.gz > /usr/share/GeoIP/GeoIP.dat
else
rm -f /usr/share/GeoIP/maxmind.dat.gz
else
echo Did not correctly download maxmind geoip country database
fi
fi
# If new file is not created, move the old file back
if [ ! -f "/usr/share/GeoIP/GeoIP.dat" ]; then
# If new file is not created, move the old file back
if [ ! -f "/usr/share/GeoIP/GeoIP.dat" ]; then
echo GeoIP.dat was not created
if [ -f "/usr/share/GeoIP/GeoIP.dat.bak" ]; then
mv /usr/share/GeoIP/GeoIP.dat.bak /usr/share/GeoIP/GeoIP.dat
fi
fi
fi
# Move old file away if it exists
if [ -f "/usr/share/GeoIP/GeoIPCity.dat" ]; then
# Move old file away if it exists
if [ -f "/usr/share/GeoIP/GeoIPCity.dat" ]; then
mv -f /usr/share/GeoIP/GeoIPCity.dat /usr/share/GeoIP/GeoIPCity.dat.bak
fi
fi
hide_output wget -P /usr/share/GeoIP/ https://dl.miyuru.lk/geoip/maxmind/city/maxmind.dat.gz
hide_output wget -P /usr/share/GeoIP/ https://dl.miyuru.lk/geoip/maxmind/city/maxmind.dat.gz
if [ -f "/usr/share/GeoIP/maxmind.dat.gz" ]; then
if [ -f "/usr/share/GeoIP/maxmind.dat.gz" ]; then
gunzip -c /usr/share/GeoIP/maxmind.dat.gz > /usr/share/GeoIP/GeoIPCity.dat
else
rm -f /usr/share/GeoIP/maxmind.dat.gz
else
echo Did not correctly download maxmind geoip city database
fi
fi
# If new file is not created, move the old file back
if [ ! -f "/usr/share/GeoIP/GeoIPCity.dat" ]; then
# If new file is not created, move the old file back
if [ ! -f "/usr/share/GeoIP/GeoIPCity.dat" ]; then
echo GeoIPCity.dat was not created
if [ -f "/usr/share/GeoIP/GeoIPCity.dat.bak" ]; then
mv /usr/share/GeoIP/GeoIPCity.dat.bak /usr/share/GeoIP/GeoIPCity.dat
fi
fi
else
echo skipping GeoIP database update
fi

View File

@ -58,7 +58,7 @@ tools/editconf.py /etc/postfix/main.cf \
smtp_bind_address=$PRIVATE_IP \
smtp_bind_address6=$PRIVATE_IPV6 \
myhostname=$PRIMARY_HOSTNAME\
smtpd_banner="\$myhostname ESMTP Hi, I'm a Power Mail-in-a-Box (Debian/Postfix)" \
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
mydestination=localhost
# Tweak some queue settings:
@ -260,19 +260,6 @@ chmod +x /etc/cron.daily/mailinabox-postgrey-whitelist
tools/editconf.py /etc/postfix/main.cf \
message_size_limit=134217728
# Store default configurations for SMTP relays:
tools/editconf.py /etc/postfix/main.cf \
smtp_sasl_auth_enable=no \
smtp_sasl_password_maps="hash:/etc/postfix/sasl_passwd" \
smtp_sasl_security_options=anonymous \
smtp_sasl_tls_security_options=anonymous \
smtp_tls_security_level=encrypt \
header_size_limit=4096000
touch /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
# Allow the two SMTP ports in the firewall.
ufw_allow smtp

View File

@ -27,9 +27,10 @@ done
# provision free TLS certificates.
apt_install duplicity python3-pip virtualenv certbot
# b2sdk is used for backblaze backups.
# boto is used for amazon aws backups.
# Both are installed outside the pipenv, so they can be used by duplicity
hide_output pip3 install --upgrade boto
hide_output pip3 install --upgrade b2sdk boto
# Create a virtualenv for the installation of Python 3 packages
# used by the management daemon.
@ -50,7 +51,7 @@ hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil \
qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver b2sdk
# CONFIGURATION
@ -69,32 +70,22 @@ rm -rf $assets_dir
mkdir -p $assets_dir
# jQuery CDN URL
jquery_version=3.5.1
jquery_version=2.1.4
jquery_url=https://code.jquery.com
# Get jQuery
wget_verify $jquery_url/jquery-$jquery_version.min.js c8e1c8b386dc5b7a9184c763c88d19a346eb3342 $assets_dir/jquery.min.js
wget_verify $jquery_url/jquery-$jquery_version.min.js 43dc554608df885a59ddeece1598c6ace434d747 $assets_dir/jquery.min.js
# Bootstrap CDN URL
bootstrap_version=4.6.0
bootstrap_version=3.3.7
bootstrap_url=https://github.com/twbs/bootstrap/releases/download/v$bootstrap_version/bootstrap-$bootstrap_version-dist.zip
# Get Bootstrap
wget_verify $bootstrap_url a1d385dc33cb415512d2f38215a554c4380dac2d /tmp/bootstrap.zip
wget_verify $bootstrap_url e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a /tmp/bootstrap.zip
unzip -q /tmp/bootstrap.zip -d $assets_dir
mv $assets_dir/bootstrap-$bootstrap_version-dist $assets_dir/bootstrap
rm -f /tmp/bootstrap.zip
# FontAwesome CDN URL
fontawesome_version=5.15.2
fontawesome_url=https://github.com/FortAwesome/Font-Awesome/releases/download/$fontawesome_version/fontawesome-free-$fontawesome_version-web.zip
# Get FontAwesome
wget_verify $fontawesome_url 2f0b3f88500238fa0be798d628a3e68c5784f165 /tmp/fontawesome.zip
unzip -q /tmp/fontawesome.zip -d $assets_dir
mv $assets_dir/fontawesome-free-$fontawesome_version-web $assets_dir/fontawesome
rm -f /tmp/fontawesome.zip
# Create an init script to start the management daemon and keep it
# running after a reboot.
cat > $inst_dir/start <<EOF;
@ -126,14 +117,3 @@ EOF
# Start the management server.
restart_service mailinabox
# FOR DEVELOPMENT PURPOSES ONLY:
# If there is a CA certificate in the folder, install it.
# MIAB will only accept a manual certificate installation
# if it is signed by a CA trusted by it.
if [[ -f mailinabox-ca.crt ]]; then
echo "Custom CA certificate detected. Installing..."
rm -f /usr/local/share/ca-certificates/mailinabox-ca.crt
cp mailinabox-ca.crt /usr/local/share/ca-certificates/
update-ca-certificates --fresh
fi

View File

@ -31,8 +31,8 @@ InstallNextcloud() {
echo "Upgrading to Nextcloud version $version"
echo
# Download and verify
wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip
# Download and verify
wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip
# Remove the current owncloud/Nextcloud
rm -rf /usr/local/lib/owncloud

View File

@ -18,10 +18,10 @@ if [ -z "${NONINTERACTIVE:-}" ]; then
hide_output pip3 install "email_validator>=1.0.0" || exit 1
message_box "Mail-in-a-Box Installation" \
"Hello and thanks for deploying a (Power) Mail-in-a-Box!
"Hello and thanks for deploying a Mail-in-a-Box!
\n\nI'm going to ask you a few questions.
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line.
\n\nNOTE: You should only install this on a brand new Debian/Ubuntu installation 100% dedicated to Mail-in-a-Box. Mail-in-a-Box will, for example, remove apache2."
\n\nNOTE: You should only install this on a brand new Ubuntu installation 100% dedicated to Mail-in-a-Box. Mail-in-a-Box will, for example, remove apache2."
fi
# The box needs a name.
@ -124,6 +124,7 @@ if [ -z "${ADMIN_HOME_IP:-}" ]; then
if [ -z "${DEFAULT_ADMIN_HOME_IP:-}" ]; then
input_box "Admin Home IP Address" \
"Enter the public IP address of the admin home, as given to you by your ISP.
This will be used to prevent banning of the administrator IP address.
\n\nAdmin Home IP address:" \
"" \
ADMIN_HOME_IP

View File

@ -112,7 +112,7 @@ apt_get_quiet autoremove
# * openssh-client: provides ssh-keygen
echo Installing system packages...
apt_install python3 python3-dev python3-pip \
apt_install python3 python3-dev python3-pip python3-setuptools \
netcat-openbsd wget curl git sudo coreutils bc \
haveged pollinate openssh-client unzip \
unattended-upgrades cron ntp fail2ban rsyslog

View File

@ -28,10 +28,11 @@ apt_install \
# Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of plugins to track
# whether we have the latest version of everything.
VERSION=1.4.10
HASH=36b2351030e1ebddb8e39190d7b0ba82b1bbec1b
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435
HTML5_NOTIFIER_VERSION=4b370e3cd60dabd2f428a26f45b677ad1b7118d5
VERSION=1.4.11
HASH=3877f0e70f29e7d0612155632e48c3db1e626be3
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 # version 5.2.0
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
CARDDAV_VERSION=4.1.1
CARDDAV_HASH=87b73661b7799b2079c28324311eddb4241242bb

View File

@ -22,8 +22,8 @@ apt_install \
phpenmod -v php imap
# Copy Z-Push into place.
VERSION=2.6.1
TARGETHASH=a4415f0dc0ed884acc8ad5c506944fc7e6d68eeb
VERSION=2.6.2
TARGETHASH=4b312d64227ef887b24d9cc8f0ae17519586f6e2
needs_update=0 #NODOC
if [ ! -f /usr/local/lib/z-push/version ]; then
needs_update=1 #NODOC
@ -102,7 +102,7 @@ EOF
# Restart service.
restart_service php$(php_version)-fpm
restart_service php7.2-fpm
# Fix states after upgrade

View File

@ -22,122 +22,116 @@
# NAME VAL
# UE
# create the new config file in memory
import sys, re
def edit_conf(filename, settings, delimiter_re, delimiter, comment_char, folded_lines = False, testing = False):
found = set()
buf = ""
input_lines = list(open(filename, "r+"))
# sanity check
if len(sys.argv) < 3:
print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c <CHARACTER>] [-t] NAME=VAL [NAME=VAL ...]")
sys.exit(1)
while len(input_lines) > 0:
line = input_lines.pop(0)
# parse command line arguments
filename = sys.argv[1]
settings = sys.argv[2:]
# If this configuration file uses folded lines, append any folded lines
# into our input buffer.
if folded_lines and line[0] not in (comment_char, " ", ""):
while len(input_lines) > 0 and input_lines[0][0] in " \t":
line += input_lines.pop(0)
# See if this line is for any settings passed on the command line.
for i in range(len(settings)):
# Check that this line contain this setting from the command-line arguments.
name, val = settings[i].split("=", 1)
m = re.match(
"(\s*)"
+ "(" + re.escape(comment_char) + "\s*)?"
+ re.escape(name) + delimiter_re + "(.*?)\s*$",
line, re.S)
if not m: continue
indent, is_comment, existing_val = m.groups()
# If this is already the setting, do nothing.
if is_comment is None and existing_val == val:
# It may be that we've already inserted this setting higher
# in the file so check for that first.
if i in found: break
buf += line
found.add(i)
break
# comment-out the existing line (also comment any folded lines)
if is_comment is None:
buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n"
else:
# the line is already commented, pass it through
buf += line
# if this option oddly appears more than once, don't add the setting again
if i in found:
break
# add the new setting
buf += indent + name + delimiter + val + "\n"
# note that we've applied this option
found.add(i)
break
else:
# If did not match any setting names, pass this line through.
buf += line
# Put any settings we didn't see at the end of the file.
for i in range(len(settings)):
if i not in found:
name, val = settings[i].split("=", 1)
buf += name + delimiter + val + "\n"
if not testing:
# Write out the new file.
with open(filename, "w") as f:
f.write(buf)
delimiter = "="
delimiter_re = r"\s*=\s*"
comment_char = "#"
folded_lines = False
testing = False
while settings[0][0] == "-" and settings[0] != "--":
opt = settings.pop(0)
if opt == "-s":
# Space is the delimiter
delimiter = " "
delimiter_re = r"\s+"
elif opt == "-w":
# Line folding is possible in this file.
folded_lines = True
elif opt == "-c":
# Specifies a different comment character.
comment_char = settings.pop(0)
elif opt == "-t":
testing = True
else:
# Just print the new file to stdout.
print(buf)
# Run standalone
if __name__ == "__main__":
# sanity check
if len(sys.argv) < 3:
print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c <CHARACTER>] [-t] NAME=VAL [NAME=VAL ...]")
print("Invalid option.")
sys.exit(1)
# parse command line arguments
filename = sys.argv[1]
settings = sys.argv[2:]
# sanity check command line
for setting in settings:
try:
name, value = setting.split("=", 1)
except:
import subprocess
print("Invalid command line: ", subprocess.list2cmdline(sys.argv))
delimiter = "="
delimiter_re = r"\s*=\s*"
comment_char = "#"
folded_lines = False
testing = False
while settings[0][0] == "-" and settings[0] != "--":
opt = settings.pop(0)
if opt == "-s":
# Space is the delimiter
delimiter = " "
delimiter_re = r"\s+"
elif opt == "-w":
# Line folding is possible in this file.
folded_lines = True
elif opt == "-c":
# Specifies a different comment character.
comment_char = settings.pop(0)
elif opt == "-t":
testing = True
# create the new config file in memory
found = set()
buf = ""
input_lines = list(open(filename))
while len(input_lines) > 0:
line = input_lines.pop(0)
# If this configuration file uses folded lines, append any folded lines
# into our input buffer.
if folded_lines and line[0] not in (comment_char, " ", ""):
while len(input_lines) > 0 and input_lines[0][0] in " \t":
line += input_lines.pop(0)
# See if this line is for any settings passed on the command line.
for i in range(len(settings)):
# Check that this line contain this setting from the command-line arguments.
name, val = settings[i].split("=", 1)
m = re.match(
"(\s*)"
+ "(" + re.escape(comment_char) + "\s*)?"
+ re.escape(name) + delimiter_re + "(.*?)\s*$",
line, re.S)
if not m: continue
indent, is_comment, existing_val = m.groups()
# If this is already the setting, do nothing.
if is_comment is None and existing_val == val:
# It may be that we've already inserted this setting higher
# in the file so check for that first.
if i in found: break
buf += line
found.add(i)
break
# comment-out the existing line (also comment any folded lines)
if is_comment is None:
buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n"
else:
print("Invalid option.")
sys.exit(1)
# the line is already commented, pass it through
buf += line
# sanity check command line
for setting in settings:
try:
name, value = setting.split("=", 1)
except:
import subprocess
print("Invalid command line: ", subprocess.list2cmdline(sys.argv))
sys.exit(1)
# if this option oddly appears more than once, don't add the setting again
if i in found:
break
edit_conf(filename, settings, delimiter_re, delimiter, comment_char, folded_lines, testing)
# add the new setting
buf += indent + name + delimiter + val + "\n"
# note that we've applied this option
found.add(i)
break
else:
# If did not match any setting names, pass this line through.
buf += line
# Put any settings we didn't see at the end of the file.
for i in range(len(settings)):
if i not in found:
name, val = settings[i].split("=", 1)
buf += name + delimiter + val + "\n"
if not testing:
# Write out the new file.
with open(filename, "w") as f:
f.write(buf)
else:
# Just print the new file to stdout.
print(buf)