mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-14 17:27:23 +01:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3ad6b4acc | ||
|
|
ec039719de | ||
|
|
14b16b2f36 | ||
|
|
cbc7e280d6 | ||
|
|
f4fa9c93a0 | ||
|
|
6c64723d7c | ||
|
|
3d21f2223e | ||
|
|
710a69b812 | ||
|
|
dd6a8d9998 | ||
|
|
9f32e5af0a | ||
|
|
298e19598b | ||
|
|
680191d7cb | ||
|
|
81d6d69b85 | ||
|
|
6df72bf4ac | ||
|
|
01f2451349 | ||
|
|
dcd971d079 | ||
|
|
4d22fb9b2a | ||
|
|
c18d58b13f | ||
|
|
b539c2df70 | ||
|
|
64fdb4ddc1 | ||
|
|
a8669197dd | ||
|
|
2412c92772 | ||
|
|
7c0ca42145 | ||
|
|
c443524ee2 | ||
|
|
e2fa01e0cf | ||
|
|
6558f05d1d | ||
|
|
ba8123f08a | ||
|
|
e2879a8eb1 | ||
|
|
eab8652225 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,6 +1,32 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
v0.08 (April 1, 2015)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
|
||||||
|
* The Roundcube vacation_sieve plugin by @arodier is now installed to make it easier to set vacation auto-reply messages from within Roundcube.
|
||||||
|
* Authentication-Results headers for DMARC, added in v0.07, were mistakenly added for outbound mail --- that's now removed.
|
||||||
|
* The Trash folder is now created automatically for new mail accounts, addressing a Roundcube error.
|
||||||
|
|
||||||
|
DNS:
|
||||||
|
|
||||||
|
* Custom DNS TXT records were not always working and they can now override the default SPF, DKIM, and DMARC records.
|
||||||
|
|
||||||
|
System:
|
||||||
|
|
||||||
|
* ownCloud updated to version 8.0.2.
|
||||||
|
* Brute-force SSH and IMAP login attempts are now prevented by properly configuring fail2ban.
|
||||||
|
* Status checks are run each night and any changes from night to night are emailed to the box administrator (the first user account).
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* The new check that system services are running mistakenly checked that the Dovecot Managesieve service is publicly accessible. Although the service binds to the public network interface we don't open the port in ufw. On some machines it seems that ufw blocks the connection from the status checks (which seems correct) and on some machines (mine) it doesn't, which is why I didn't notice the problem.
|
||||||
|
* The current backup chain will now try to predict how many days until it is deleted (always at least 3 days after the next full backup).
|
||||||
|
* The list of aliases that forward to a user are removed from the Mail Users page because when there are many alises it is slow and times-out.
|
||||||
|
* Some status check errors are turned into warnings, especially those that might not apply if External DNS is used.
|
||||||
|
|
||||||
v0.07 (February 28, 2015)
|
v0.07 (February 28, 2015)
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|||||||
22
conf/fail2ban/dovecotimap.conf
Normal file
22
conf/fail2ban/dovecotimap.conf
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Fail2Ban filter Dovecot authentication and pop3/imap server
|
||||||
|
# For Mail-in-a-Box
|
||||||
|
|
||||||
|
[INCLUDES]
|
||||||
|
|
||||||
|
before = common.conf
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
|
||||||
|
_daemon = (auth|dovecot(-auth)?|auth-worker)
|
||||||
|
|
||||||
|
failregex = ^%(__prefix_line)s(pop3|imap)-login: (Info: )?(Aborted login|Disconnected)(: Inactivity)? \(((no auth attempts|auth failed, \d+ attempts)( in \d+ secs)?|tried to use (disabled|disallowed) \S+ auth)\):( user=<\S*>,)?( method=\S+,)? rip=<HOST>, lip=(\d{1,3}\.){3}\d{1,3}(, TLS( handshaking)?(: Disconnected)?)?(, session=<\S+>)?\s*$
|
||||||
|
|
||||||
|
ignoreregex =
|
||||||
|
|
||||||
|
# DEV Notes:
|
||||||
|
# * the first regex is essentially a copy of pam-generic.conf
|
||||||
|
# * Probably doesn't do dovecot sql/ldap backends properly
|
||||||
|
#
|
||||||
|
# Author: Martin Waschbuesch
|
||||||
|
# Daniel Black (rewrote with begin and end anchors)
|
||||||
|
# Mail-in-a-Box (swapped session=...)
|
||||||
34
conf/fail2ban/jail.local
Normal file
34
conf/fail2ban/jail.local
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Fail2Ban configuration file.
|
||||||
|
# For Mail-in-a-Box
|
||||||
|
[DEFAULT]
|
||||||
|
|
||||||
|
# bantime in seconds
|
||||||
|
bantime = 60
|
||||||
|
|
||||||
|
# This should ban dumb brute-force attacks, not oblivious users.
|
||||||
|
findtime = 30
|
||||||
|
maxretry = 20
|
||||||
|
|
||||||
|
#
|
||||||
|
# JAILS
|
||||||
|
#
|
||||||
|
|
||||||
|
[ssh]
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
logpath = /var/log/auth.log
|
||||||
|
maxretry = 20
|
||||||
|
|
||||||
|
[ssh-ddos]
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
maxretry = 20
|
||||||
|
|
||||||
|
[sasl]
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[dovecot]
|
||||||
|
|
||||||
|
enabled = true
|
||||||
|
filter = dovecotimap
|
||||||
@@ -67,9 +67,29 @@ def backup_status(env):
|
|||||||
# This is relied on by should_force_full() and the next step.
|
# This is relied on by should_force_full() and the next step.
|
||||||
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
|
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
|
||||||
|
|
||||||
|
# Get the average size of incremental backups and the size of the
|
||||||
|
# most recent full backup.
|
||||||
|
incremental_count = 0
|
||||||
|
incremental_size = 0
|
||||||
|
first_full_size = None
|
||||||
|
for bak in backups:
|
||||||
|
if bak["full"]:
|
||||||
|
first_full_size = bak["size"]
|
||||||
|
break
|
||||||
|
incremental_count += 1
|
||||||
|
incremental_size += bak["size"]
|
||||||
|
|
||||||
|
# Predict how many more increments until the next full backup,
|
||||||
|
# and add to that the time we hold onto backups, to predict
|
||||||
|
# how long the most recent full backup+increments will be held
|
||||||
|
# onto. Round up since the backup occurs on the night following
|
||||||
|
# when the threshold is met.
|
||||||
|
deleted_in = None
|
||||||
|
if incremental_count > 0 and first_full_size is not None:
|
||||||
|
deleted_in = "approx. %d days" % round(keep_backups_for_days + (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count) + .5)
|
||||||
|
|
||||||
# When will a backup be deleted?
|
# When will a backup be deleted?
|
||||||
saw_full = False
|
saw_full = False
|
||||||
deleted_in = None
|
|
||||||
days_ago = now - datetime.timedelta(days=keep_backups_for_days)
|
days_ago = now - datetime.timedelta(days=keep_backups_for_days)
|
||||||
for bak in backups:
|
for bak in backups:
|
||||||
if deleted_in:
|
if deleted_in:
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ def system_status():
|
|||||||
def print_line(self, message, monospace=False):
|
def print_line(self, message, monospace=False):
|
||||||
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
|
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
|
||||||
output = WebOutput()
|
output = WebOutput()
|
||||||
run_checks(env, output, pool)
|
run_checks(False, env, output, pool)
|
||||||
return json_response(output.items)
|
return json_response(output.items)
|
||||||
|
|
||||||
@app.route('/system/updates')
|
@app.route('/system/updates')
|
||||||
|
|||||||
@@ -183,10 +183,6 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
# The MX record says where email for the domain should be delivered: Here!
|
# The MX record says where email for the domain should be delivered: Here!
|
||||||
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
|
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
|
||||||
|
|
||||||
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
|
||||||
# the domain, and no one else.
|
|
||||||
records.append((None, "TXT", 'v=spf1 mx -all', "Recommended. Specifies that only the box is permitted to send @%s mail." % domain))
|
|
||||||
|
|
||||||
# Add DNS records for any subdomains of this domain. We should not have a zone for
|
# Add DNS records for any subdomains of this domain. We should not have a zone for
|
||||||
# both a domain and one of its subdomains.
|
# both a domain and one of its subdomains.
|
||||||
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
||||||
@@ -207,6 +203,7 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# The user may set other records that don't conflict with our settings.
|
# The user may set other records that don't conflict with our settings.
|
||||||
|
# Don't put any TXT records above this line, or it'll prevent any custom TXT records.
|
||||||
for qname, rtype, value in get_custom_records(domain, additional_records, env):
|
for qname, rtype, value in get_custom_records(domain, additional_records, env):
|
||||||
if has_rec(qname, rtype): continue
|
if has_rec(qname, rtype): continue
|
||||||
records.append((qname, rtype, value, "(Set by user.)"))
|
records.append((qname, rtype, value, "(Set by user.)"))
|
||||||
@@ -229,15 +226,25 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
if not has_rec(qname, rtype) and not has_rec(qname, "CNAME") and not has_rec(qname, "A"):
|
if not has_rec(qname, rtype) and not has_rec(qname, "CNAME") and not has_rec(qname, "A"):
|
||||||
records.append((qname, rtype, value, explanation))
|
records.append((qname, rtype, value, explanation))
|
||||||
|
|
||||||
|
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
||||||
|
# the domain, and no one else.
|
||||||
|
# Skip if the user has set a custom SPF record.
|
||||||
|
if not has_rec(None, "TXT", prefix="v=spf1 "):
|
||||||
|
records.append((None, "TXT", 'v=spf1 mx -all', "Recommended. Specifies that only the box is permitted to send @%s mail." % domain))
|
||||||
|
|
||||||
# Append the DKIM TXT record to the zone as generated by OpenDKIM.
|
# Append the DKIM TXT record to the zone as generated by OpenDKIM.
|
||||||
|
# Skip if the user has set a DKIM record already.
|
||||||
opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt')
|
opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt')
|
||||||
with open(opendkim_record_file) as orf:
|
with open(opendkim_record_file) as orf:
|
||||||
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( "([^"]+)"\s+"([^"]+)"\s*\)', orf.read(), re.S)
|
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( "([^"]+)"\s+"([^"]+)"\s*\)', orf.read(), re.S)
|
||||||
val = m.group(2) + m.group(3)
|
val = m.group(2) + m.group(3)
|
||||||
records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
|
if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "):
|
||||||
|
records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
|
||||||
|
|
||||||
# Append a DMARC record.
|
# Append a DMARC record.
|
||||||
records.append(("_dmarc", "TXT", 'v=DMARC1; p=quarantine', "Optional. Specifies that mail that does not originate from the box but claims to be from @%s is suspect and should be quarantined by the recipient's mail system." % domain))
|
# Skip if the user has set a DMARC record already.
|
||||||
|
if not has_rec("_dmarc", "TXT", prefix="v=DMARC1; "):
|
||||||
|
records.append(("_dmarc", "TXT", 'v=DMARC1; p=quarantine', "Optional. Specifies that mail that does not originate from the box but claims to be from @%s is suspect and should be quarantined by the recipient's mail system." % domain))
|
||||||
|
|
||||||
# For any subdomain with an A record but no SPF or DMARC record, add strict policy records.
|
# For any subdomain with an A record but no SPF or DMARC record, add strict policy records.
|
||||||
all_resolvable_qnames = set(r[0] for r in records if r[1] in ("A", "AAAA"))
|
all_resolvable_qnames = set(r[0] for r in records if r[1] in ("A", "AAAA"))
|
||||||
@@ -691,7 +698,7 @@ def set_custom_dns_record(qname, rtype, value, env):
|
|||||||
v = ipaddress.ip_address(value)
|
v = ipaddress.ip_address(value)
|
||||||
if rtype == "A" and not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.")
|
if rtype == "A" and not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.")
|
||||||
if rtype == "AAAA" and not isinstance(v, ipaddress.IPv6Address): raise ValueError("That's an IPv4 address.")
|
if rtype == "AAAA" and not isinstance(v, ipaddress.IPv6Address): raise ValueError("That's an IPv4 address.")
|
||||||
elif rtype in ("CNAME", "TXT", "SRV"):
|
elif rtype in ("CNAME", "TXT", "SRV", "MX"):
|
||||||
# anything goes
|
# anything goes
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -91,10 +91,6 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
|
|||||||
# email: "name@domain.tld",
|
# email: "name@domain.tld",
|
||||||
# privileges: [ "priv1", "priv2", ... ],
|
# privileges: [ "priv1", "priv2", ... ],
|
||||||
# status: "active",
|
# status: "active",
|
||||||
# aliases: [
|
|
||||||
# ("alias@domain.tld", ["indirect.alias@domain.tld", ...]),
|
|
||||||
# ...
|
|
||||||
# ]
|
|
||||||
# },
|
# },
|
||||||
# ...
|
# ...
|
||||||
# ]
|
# ]
|
||||||
@@ -102,9 +98,6 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
|
|||||||
# ...
|
# ...
|
||||||
# ]
|
# ]
|
||||||
|
|
||||||
# Pre-load all aliases.
|
|
||||||
aliases = get_mail_alias_map(env)
|
|
||||||
|
|
||||||
# Get users and their privileges.
|
# Get users and their privileges.
|
||||||
users = []
|
users = []
|
||||||
active_accounts = set()
|
active_accounts = set()
|
||||||
@@ -121,10 +114,6 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
|
|||||||
users.append(user)
|
users.append(user)
|
||||||
|
|
||||||
if with_slow_info:
|
if with_slow_info:
|
||||||
user["aliases"] = [
|
|
||||||
(alias, sorted(evaluate_mail_alias_map(alias, aliases, env)))
|
|
||||||
for alias in aliases.get(email.lower(), [])
|
|
||||||
]
|
|
||||||
user["mailbox_size"] = utils.du(os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes', *reversed(email.split("@"))))
|
user["mailbox_size"] = utils.du(os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes', *reversed(email.split("@"))))
|
||||||
|
|
||||||
# Add in archived accounts.
|
# Add in archived accounts.
|
||||||
@@ -230,21 +219,6 @@ def get_mail_aliases_ex(env):
|
|||||||
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["source"]))
|
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["source"]))
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
def get_mail_alias_map(env):
|
|
||||||
aliases = { }
|
|
||||||
for alias, targets in get_mail_aliases(env):
|
|
||||||
for em in targets.split(","):
|
|
||||||
em = em.strip().lower()
|
|
||||||
aliases.setdefault(em, []).append(alias)
|
|
||||||
return aliases
|
|
||||||
|
|
||||||
def evaluate_mail_alias_map(email, aliases, env):
|
|
||||||
ret = set()
|
|
||||||
for alias in aliases.get(email.lower(), []):
|
|
||||||
ret.add(alias)
|
|
||||||
ret |= evaluate_mail_alias_map(alias, aliases, env)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_domain(emailaddr):
|
def get_domain(emailaddr):
|
||||||
return emailaddr.split('@', 1)[1]
|
return emailaddr.split('@', 1)[1]
|
||||||
|
|
||||||
@@ -261,8 +235,10 @@ def add_mail_user(email, pw, privs, env):
|
|||||||
# validate email
|
# validate email
|
||||||
if email.strip() == "":
|
if email.strip() == "":
|
||||||
return ("No email address provided.", 400)
|
return ("No email address provided.", 400)
|
||||||
if not validate_email(email, mode='user'):
|
elif not validate_email(email):
|
||||||
return ("Invalid email address.", 400)
|
return ("Invalid email address.", 400)
|
||||||
|
elif not validate_email(email, mode='user'):
|
||||||
|
return ("User account email addresses may only use the ASCII letters A-Z, the digits 0-9, underscore (_), hyphen (-), and period (.).", 400)
|
||||||
|
|
||||||
validate_password(pw)
|
validate_password(pw)
|
||||||
|
|
||||||
@@ -291,9 +267,11 @@ def add_mail_user(email, pw, privs, env):
|
|||||||
# write databasebefore next step
|
# write databasebefore next step
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Create the user's INBOX, Spam, and Drafts folders, and subscribe them.
|
# Create & subscribe the user's INBOX, Trash, Spam, and Drafts folders.
|
||||||
# K-9 mail will poll every 90 seconds if a Drafts folder does not exist, so create it
|
# * Our sieve rule for spam expects that the Spam folder exists.
|
||||||
# to avoid unnecessary polling.
|
# * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359).
|
||||||
|
# * K-9 mail will poll every 90 seconds if a Drafts folder does not exist, so create it
|
||||||
|
# to avoid unnecessary polling.
|
||||||
|
|
||||||
# Check if the mailboxes exist before creating them. When creating a user that had previously
|
# Check if the mailboxes exist before creating them. When creating a user that had previously
|
||||||
# been deleted, the mailboxes will still exist because they are still on disk.
|
# been deleted, the mailboxes will still exist because they are still on disk.
|
||||||
@@ -304,7 +282,7 @@ def add_mail_user(email, pw, privs, env):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return ("Failed to initialize the user: " + e.output.decode("utf8"), 400)
|
return ("Failed to initialize the user: " + e.output.decode("utf8"), 400)
|
||||||
|
|
||||||
for folder in ("INBOX", "Spam", "Drafts"):
|
for folder in ("INBOX", "Trash", "Spam", "Drafts"):
|
||||||
if folder not in existing_mboxes:
|
if folder not in existing_mboxes:
|
||||||
utils.shell('check_call', ["doveadm", "mailbox", "create", "-u", email, "-s", folder])
|
utils.shell('check_call', ["doveadm", "mailbox", "create", "-u", email, "-s", folder])
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
__ALL__ = ['check_certificate']
|
__ALL__ = ['check_certificate']
|
||||||
|
|
||||||
import os, os.path, re, subprocess, datetime, multiprocessing.pool
|
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
||||||
|
|
||||||
import dns.reversename, dns.resolver
|
import dns.reversename, dns.resolver
|
||||||
import dateutil.parser, dateutil.tz
|
import dateutil.parser, dateutil.tz
|
||||||
@@ -17,7 +17,7 @@ from mailconfig import get_mail_domains, get_mail_aliases
|
|||||||
|
|
||||||
from utils import shell, sort_domains, load_env_vars_from_file
|
from utils import shell, sort_domains, load_env_vars_from_file
|
||||||
|
|
||||||
def run_checks(env, output, pool):
|
def run_checks(rounded_values, env, output, pool):
|
||||||
# run systems checks
|
# run systems checks
|
||||||
output.add_heading("System")
|
output.add_heading("System")
|
||||||
|
|
||||||
@@ -33,12 +33,12 @@ def run_checks(env, output, pool):
|
|||||||
# that in run_services checks.)
|
# that in run_services checks.)
|
||||||
shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True)
|
shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True)
|
||||||
|
|
||||||
run_system_checks(env, output)
|
run_system_checks(rounded_values, env, output)
|
||||||
|
|
||||||
# perform other checks asynchronously
|
# perform other checks asynchronously
|
||||||
|
|
||||||
run_network_checks(env, output)
|
run_network_checks(env, output)
|
||||||
run_domain_checks(env, output, pool)
|
run_domain_checks(rounded_values, env, output, pool)
|
||||||
|
|
||||||
def get_ssh_port():
|
def get_ssh_port():
|
||||||
# Returns ssh port
|
# Returns ssh port
|
||||||
@@ -64,7 +64,7 @@ def run_services_checks(env, output, pool):
|
|||||||
{ "name": "OpenDKIM", "port": 8891, "public": False, },
|
{ "name": "OpenDKIM", "port": 8891, "public": False, },
|
||||||
{ "name": "OpenDMARC", "port": 8893, "public": False, },
|
{ "name": "OpenDMARC", "port": 8893, "public": False, },
|
||||||
{ "name": "Memcached", "port": 11211, "public": False, },
|
{ "name": "Memcached", "port": 11211, "public": False, },
|
||||||
{ "name": "Sieve (dovecot)", "port": 4190, "public": True, },
|
{ "name": "Sieve (dovecot)", "port": 4190, "public": False, },
|
||||||
{ "name": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, },
|
{ "name": "Mail-in-a-Box Management Daemon", "port": 10222, "public": False, },
|
||||||
|
|
||||||
{ "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
|
{ "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
|
||||||
@@ -133,11 +133,11 @@ def check_service(i, service, env):
|
|||||||
|
|
||||||
return (i, running, fatal, output)
|
return (i, running, fatal, output)
|
||||||
|
|
||||||
def run_system_checks(env, output):
|
def run_system_checks(rounded_values, env, output):
|
||||||
check_ssh_password(env, output)
|
check_ssh_password(env, output)
|
||||||
check_software_updates(env, output)
|
check_software_updates(env, output)
|
||||||
check_system_aliases(env, output)
|
check_system_aliases(env, output)
|
||||||
check_free_disk_space(env, output)
|
check_free_disk_space(rounded_values, env, output)
|
||||||
|
|
||||||
def check_ssh_password(env, output):
|
def check_ssh_password(env, output):
|
||||||
# Check that SSH login with password is disabled. The openssh-server
|
# Check that SSH login with password is disabled. The openssh-server
|
||||||
@@ -172,12 +172,15 @@ def check_system_aliases(env, output):
|
|||||||
# admin email is automatically directed.
|
# admin email is automatically directed.
|
||||||
check_alias_exists("administrator@" + env['PRIMARY_HOSTNAME'], env, output)
|
check_alias_exists("administrator@" + env['PRIMARY_HOSTNAME'], env, output)
|
||||||
|
|
||||||
def check_free_disk_space(env, output):
|
def check_free_disk_space(rounded_values, env, output):
|
||||||
# Check free disk space.
|
# Check free disk space.
|
||||||
st = os.statvfs(env['STORAGE_ROOT'])
|
st = os.statvfs(env['STORAGE_ROOT'])
|
||||||
bytes_total = st.f_blocks * st.f_frsize
|
bytes_total = st.f_blocks * st.f_frsize
|
||||||
bytes_free = st.f_bavail * st.f_frsize
|
bytes_free = st.f_bavail * st.f_frsize
|
||||||
disk_msg = "The disk has %s GB space remaining." % str(round(bytes_free/1024.0/1024.0/1024.0*10.0)/10.0)
|
if not rounded_values:
|
||||||
|
disk_msg = "The disk has %s GB space remaining." % str(round(bytes_free/1024.0/1024.0/1024.0*10.0)/10)
|
||||||
|
else:
|
||||||
|
disk_msg = "The disk has less than %s%% space left." % str(round(bytes_free/bytes_total/10 + .5)*10)
|
||||||
if bytes_free > .3 * bytes_total:
|
if bytes_free > .3 * bytes_total:
|
||||||
output.print_ok(disk_msg)
|
output.print_ok(disk_msg)
|
||||||
elif bytes_free > .15 * bytes_total:
|
elif bytes_free > .15 * bytes_total:
|
||||||
@@ -215,7 +218,7 @@ def run_network_checks(env, output):
|
|||||||
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
|
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
|
||||||
% (env['PUBLIC_IP'], zen, env['PUBLIC_IP']))
|
% (env['PUBLIC_IP'], zen, env['PUBLIC_IP']))
|
||||||
|
|
||||||
def run_domain_checks(env, output, pool):
|
def run_domain_checks(rounded_time, env, output, pool):
|
||||||
# Get the list of domains we handle mail for.
|
# Get the list of domains we handle mail for.
|
||||||
mail_domains = get_mail_domains(env)
|
mail_domains = get_mail_domains(env)
|
||||||
|
|
||||||
@@ -230,17 +233,17 @@ def run_domain_checks(env, output, pool):
|
|||||||
|
|
||||||
# Serial version:
|
# Serial version:
|
||||||
#for domain in sort_domains(domains_to_check, env):
|
#for domain in sort_domains(domains_to_check, env):
|
||||||
# run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
|
# run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
|
||||||
|
|
||||||
# Parallelize the checks across a worker pool.
|
# Parallelize the checks across a worker pool.
|
||||||
args = ((domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
|
args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
|
||||||
for domain in domains_to_check)
|
for domain in domains_to_check)
|
||||||
ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1)
|
ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1)
|
||||||
ret = dict(ret) # (domain, output) => { domain: output }
|
ret = dict(ret) # (domain, output) => { domain: output }
|
||||||
for domain in sort_domains(ret, env):
|
for domain in sort_domains(ret, env):
|
||||||
ret[domain].playback(output)
|
ret[domain].playback(output)
|
||||||
|
|
||||||
def run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains):
|
def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains):
|
||||||
output = BufferedOutput()
|
output = BufferedOutput()
|
||||||
|
|
||||||
output.add_heading(domain)
|
output.add_heading(domain)
|
||||||
@@ -255,7 +258,7 @@ def run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_do
|
|||||||
check_mail_domain(domain, env, output)
|
check_mail_domain(domain, env, output)
|
||||||
|
|
||||||
if domain in web_domains:
|
if domain in web_domains:
|
||||||
check_web_domain(domain, env, output)
|
check_web_domain(domain, rounded_time, env, output)
|
||||||
|
|
||||||
if domain in dns_domains:
|
if domain in dns_domains:
|
||||||
check_dns_zone_suggestions(domain, env, output, dns_zonefiles)
|
check_dns_zone_suggestions(domain, env, output, dns_zonefiles)
|
||||||
@@ -264,27 +267,38 @@ def run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_do
|
|||||||
|
|
||||||
def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
||||||
# If a DS record is set on the zone containing this domain, check DNSSEC now.
|
# If a DS record is set on the zone containing this domain, check DNSSEC now.
|
||||||
|
has_dnssec = False
|
||||||
for zone in dns_domains:
|
for zone in dns_domains:
|
||||||
if zone == domain or domain.endswith("." + zone):
|
if zone == domain or domain.endswith("." + zone):
|
||||||
if query_dns(zone, "DS", nxdomain=None) is not None:
|
if query_dns(zone, "DS", nxdomain=None) is not None:
|
||||||
|
has_dnssec = True
|
||||||
check_dnssec(zone, env, output, dns_zonefiles, is_checking_primary=True)
|
check_dnssec(zone, env, output, dns_zonefiles, is_checking_primary=True)
|
||||||
|
|
||||||
|
ip = query_dns(domain, "A")
|
||||||
|
ns_ips = query_dns("ns1." + domain, "A") + '/' + query_dns("ns2." + domain, "A")
|
||||||
|
|
||||||
# Check that the ns1/ns2 hostnames resolve to A records. This information probably
|
# Check that the ns1/ns2 hostnames resolve to A records. This information probably
|
||||||
# comes from the TLD since the information is set at the registrar as glue records.
|
# comes from the TLD since the information is set at the registrar as glue records.
|
||||||
# We're probably not actually checking that here but instead checking that we, as
|
# We're probably not actually checking that here but instead checking that we, as
|
||||||
# the nameserver, are reporting the right info --- but if the glue is incorrect this
|
# the nameserver, are reporting the right info --- but if the glue is incorrect this
|
||||||
# will probably fail.
|
# will probably fail.
|
||||||
ip = query_dns("ns1." + domain, "A") + '/' + query_dns("ns2." + domain, "A")
|
if ns_ips == env['PUBLIC_IP'] + '/' + env['PUBLIC_IP']:
|
||||||
if ip == env['PUBLIC_IP'] + '/' + env['PUBLIC_IP']:
|
|
||||||
output.print_ok("Nameserver glue records are correct at registrar. [ns1/ns2.%s => %s]" % (env['PRIMARY_HOSTNAME'], env['PUBLIC_IP']))
|
output.print_ok("Nameserver glue records are correct at registrar. [ns1/ns2.%s => %s]" % (env['PRIMARY_HOSTNAME'], env['PUBLIC_IP']))
|
||||||
|
|
||||||
|
elif ip == env['PUBLIC_IP']:
|
||||||
|
# The NS records are not what we expect, but the domain resolves correctly, so
|
||||||
|
# the user may have set up external DNS. List this discrepancy as a warning.
|
||||||
|
output.print_warning("""Nameserver glue records (ns1.%s and ns2.%s) should be configured at your domain name
|
||||||
|
registrar as having the IP address of this box (%s). They currently report addresses of %s. If you have set up External DNS, this may be OK."""
|
||||||
|
% (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
output.print_error("""Nameserver glue records are incorrect. The ns1.%s and ns2.%s nameservers must be configured at your domain name
|
output.print_error("""Nameserver glue records are incorrect. The ns1.%s and ns2.%s nameservers must be configured at your domain name
|
||||||
registrar as having the IP address %s. They currently report addresses of %s. It may take several hours for
|
registrar as having the IP address %s. They currently report addresses of %s. It may take several hours for
|
||||||
public DNS to update after a change."""
|
public DNS to update after a change."""
|
||||||
% (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ip))
|
% (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips))
|
||||||
|
|
||||||
# Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP in public DNS.
|
# Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP in public DNS.
|
||||||
ip = query_dns(domain, "A")
|
|
||||||
if ip == env['PUBLIC_IP']:
|
if ip == env['PUBLIC_IP']:
|
||||||
output.print_ok("Domain resolves to box's IP address. [%s => %s]" % (env['PRIMARY_HOSTNAME'], env['PUBLIC_IP']))
|
output.print_ok("Domain resolves to box's IP address. [%s => %s]" % (env['PRIMARY_HOSTNAME'], env['PUBLIC_IP']))
|
||||||
else:
|
else:
|
||||||
@@ -310,7 +324,10 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
|||||||
if tlsa25 == tlsa25_expected:
|
if tlsa25 == tlsa25_expected:
|
||||||
output.print_ok("""The DANE TLSA record for incoming mail is correct (%s).""" % tlsa_qname,)
|
output.print_ok("""The DANE TLSA record for incoming mail is correct (%s).""" % tlsa_qname,)
|
||||||
elif tlsa25 is None:
|
elif tlsa25 is None:
|
||||||
output.print_error("""The DANE TLSA record for incoming mail is not set. This is optional.""")
|
if has_dnssec:
|
||||||
|
# Omit a warning about it not being set if DNSSEC isn't enabled,
|
||||||
|
# since TLSA shouldn't be used without DNSSEC.
|
||||||
|
output.print_warning("""The DANE TLSA record for incoming mail is not set. This is optional.""")
|
||||||
else:
|
else:
|
||||||
output.print_error("""The DANE TLSA record for incoming mail (%s) is not correct. It is '%s' but it should be '%s'.
|
output.print_error("""The DANE TLSA record for incoming mail (%s) is not correct. It is '%s' but it should be '%s'.
|
||||||
It may take several hours for public DNS to update after a change."""
|
It may take several hours for public DNS to update after a change."""
|
||||||
@@ -338,6 +355,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
|||||||
# whois information -- we may be getting the NS records from us rather than
|
# whois information -- we may be getting the NS records from us rather than
|
||||||
# the TLD, and so we're not actually checking the TLD. For that we'd need
|
# the TLD, and so we're not actually checking the TLD. For that we'd need
|
||||||
# to do a DNS trace.
|
# to do a DNS trace.
|
||||||
|
ip = query_dns(domain, "A")
|
||||||
custom_dns = get_custom_dns_config(env)
|
custom_dns = get_custom_dns_config(env)
|
||||||
existing_ns = query_dns(domain, "NS")
|
existing_ns = query_dns(domain, "NS")
|
||||||
correct_ns = "; ".join(sorted([
|
correct_ns = "; ".join(sorted([
|
||||||
@@ -346,6 +364,11 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
|||||||
]))
|
]))
|
||||||
if existing_ns.lower() == correct_ns.lower():
|
if existing_ns.lower() == correct_ns.lower():
|
||||||
output.print_ok("Nameservers are set correctly at registrar. [%s]" % correct_ns)
|
output.print_ok("Nameservers are set correctly at registrar. [%s]" % correct_ns)
|
||||||
|
elif ip == env['PUBLIC_IP']:
|
||||||
|
# The domain resolves correctly, so maybe the user is using External DNS.
|
||||||
|
output.print_warning("""The nameservers set on this domain at your domain name registrar should be %s. They are currently %s.
|
||||||
|
If you are using External DNS, this may be OK."""
|
||||||
|
% (correct_ns, existing_ns) )
|
||||||
else:
|
else:
|
||||||
output.print_error("""The nameservers set on this domain are incorrect. They are currently %s. Use your domain name registrar's
|
output.print_error("""The nameservers set on this domain are incorrect. They are currently %s. Use your domain name registrar's
|
||||||
control panel to set the nameservers to %s."""
|
control panel to set the nameservers to %s."""
|
||||||
@@ -384,7 +407,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
|
|||||||
else:
|
else:
|
||||||
if ds == None:
|
if ds == None:
|
||||||
if is_checking_primary: return
|
if is_checking_primary: return
|
||||||
output.print_error("""This domain's DNSSEC DS record is not set. The DS record is optional. The DS record activates DNSSEC.
|
output.print_warning("""This domain's DNSSEC DS record is not set. The DS record is optional. The DS record activates DNSSEC.
|
||||||
To set a DS record, you must follow the instructions provided by your domain name registrar and provide to them this information:""")
|
To set a DS record, you must follow the instructions provided by your domain name registrar and provide to them this information:""")
|
||||||
else:
|
else:
|
||||||
if is_checking_primary:
|
if is_checking_primary:
|
||||||
@@ -472,7 +495,7 @@ def check_mail_domain(domain, env, output):
|
|||||||
which may prevent recipients from receiving your mail.
|
which may prevent recipients from receiving your mail.
|
||||||
See http://www.spamhaus.org/dbl/ and http://www.spamhaus.org/query/domain/%s.""" % (dbl, domain))
|
See http://www.spamhaus.org/dbl/ and http://www.spamhaus.org/query/domain/%s.""" % (dbl, domain))
|
||||||
|
|
||||||
def check_web_domain(domain, env, output):
|
def check_web_domain(domain, rounded_time, env, output):
|
||||||
# See if the domain's A record resolves to our PUBLIC_IP. This is already checked
|
# See if the domain's A record resolves to our PUBLIC_IP. This is already checked
|
||||||
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
|
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
|
||||||
# other domains, it is required to access its website.
|
# other domains, it is required to access its website.
|
||||||
@@ -488,7 +511,7 @@ def check_web_domain(domain, env, output):
|
|||||||
# We need a SSL certificate for PRIMARY_HOSTNAME because that's where the
|
# We need a SSL certificate for PRIMARY_HOSTNAME because that's where the
|
||||||
# user will log in with IMAP or webmail. Any other domain we serve a
|
# user will log in with IMAP or webmail. Any other domain we serve a
|
||||||
# website for also needs a signed certificate.
|
# website for also needs a signed certificate.
|
||||||
check_ssl_cert(domain, env, output)
|
check_ssl_cert(domain, rounded_time, env, output)
|
||||||
|
|
||||||
def query_dns(qname, rtype, nxdomain='[Not Set]'):
|
def query_dns(qname, rtype, nxdomain='[Not Set]'):
|
||||||
# Make the qname absolute by appending a period. Without this, dns.resolver.query
|
# Make the qname absolute by appending a period. Without this, dns.resolver.query
|
||||||
@@ -515,7 +538,7 @@ def query_dns(qname, rtype, nxdomain='[Not Set]'):
|
|||||||
# can compare to a well known order.
|
# can compare to a well known order.
|
||||||
return "; ".join(sorted(str(r).rstrip('.') for r in response))
|
return "; ".join(sorted(str(r).rstrip('.') for r in response))
|
||||||
|
|
||||||
def check_ssl_cert(domain, env, output):
|
def check_ssl_cert(domain, rounded_time, env, output):
|
||||||
# Check that SSL certificate is signed.
|
# Check that SSL certificate is signed.
|
||||||
|
|
||||||
# Skip the check if the A record is not pointed here.
|
# Skip the check if the A record is not pointed here.
|
||||||
@@ -530,7 +553,7 @@ def check_ssl_cert(domain, env, output):
|
|||||||
|
|
||||||
# Check that the certificate is good.
|
# Check that the certificate is good.
|
||||||
|
|
||||||
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
|
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, rounded_time=rounded_time)
|
||||||
|
|
||||||
if cert_status == "OK":
|
if cert_status == "OK":
|
||||||
# The certificate is ok. The details has expiry info.
|
# The certificate is ok. The details has expiry info.
|
||||||
@@ -569,7 +592,7 @@ def check_ssl_cert(domain, env, output):
|
|||||||
output.print_line(cert_status_details)
|
output.print_line(cert_status_details)
|
||||||
output.print_line("")
|
output.print_line("")
|
||||||
|
|
||||||
def check_certificate(domain, ssl_certificate, ssl_private_key):
|
def check_certificate(domain, ssl_certificate, ssl_private_key, rounded_time=False):
|
||||||
# Use openssl verify to check the status of a certificate.
|
# Use openssl verify to check the status of a certificate.
|
||||||
|
|
||||||
# First check that the certificate is for the right domain. The domain
|
# First check that the certificate is for the right domain. The domain
|
||||||
@@ -680,7 +703,15 @@ def check_certificate(domain, ssl_certificate, ssl_private_key):
|
|||||||
# But is it expiring soon?
|
# But is it expiring soon?
|
||||||
now = datetime.datetime.now(dateutil.tz.tzlocal())
|
now = datetime.datetime.now(dateutil.tz.tzlocal())
|
||||||
ndays = (cert_expiration_date-now).days
|
ndays = (cert_expiration_date-now).days
|
||||||
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
if not rounded_time or ndays < 7:
|
||||||
|
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
||||||
|
elif ndays <= 14:
|
||||||
|
expiry_info = "The certificate expires in less than two weeks, on %s." % cert_expiration_date.strftime("%x")
|
||||||
|
elif ndays <= 31:
|
||||||
|
expiry_info = "The certificate expires in less than a month, on %s." % cert_expiration_date.strftime("%x")
|
||||||
|
else:
|
||||||
|
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
|
||||||
|
|
||||||
if ndays <= 31:
|
if ndays <= 31:
|
||||||
return ("The certificate is expiring soon: " + expiry_info, None)
|
return ("The certificate is expiring soon: " + expiry_info, None)
|
||||||
|
|
||||||
@@ -721,17 +752,109 @@ def list_apt_updates(apt_update=True):
|
|||||||
|
|
||||||
return pkgs
|
return pkgs
|
||||||
|
|
||||||
|
def run_and_output_changes(env, pool, send_via_email):
|
||||||
|
import json
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
class ConsoleOutput:
|
if not send_via_email:
|
||||||
try:
|
out = ConsoleOutput()
|
||||||
terminal_columns = int(shell('check_output', ['stty', 'size']).split()[1])
|
else:
|
||||||
except:
|
import io
|
||||||
terminal_columns = 76
|
out = FileOutput(io.StringIO(""), 70)
|
||||||
|
|
||||||
|
# Run status checks.
|
||||||
|
cur = BufferedOutput()
|
||||||
|
run_checks(True, env, cur, pool)
|
||||||
|
|
||||||
|
# Load previously saved status checks.
|
||||||
|
cache_fn = "/var/cache/mailinabox/status_checks.json"
|
||||||
|
if os.path.exists(cache_fn):
|
||||||
|
prev = json.load(open(cache_fn))
|
||||||
|
|
||||||
|
# Group the serial output into categories by the headings.
|
||||||
|
def group_by_heading(lines):
|
||||||
|
from collections import OrderedDict
|
||||||
|
ret = OrderedDict()
|
||||||
|
k = []
|
||||||
|
ret["No Category"] = k
|
||||||
|
for line_type, line_args, line_kwargs in lines:
|
||||||
|
if line_type == "add_heading":
|
||||||
|
k = []
|
||||||
|
ret[line_args[0]] = k
|
||||||
|
else:
|
||||||
|
k.append((line_type, line_args, line_kwargs))
|
||||||
|
return ret
|
||||||
|
prev_status = group_by_heading(prev)
|
||||||
|
cur_status = group_by_heading(cur.buf)
|
||||||
|
|
||||||
|
# Compare the previous to the current status checks
|
||||||
|
# category by category.
|
||||||
|
for category, cur_lines in cur_status.items():
|
||||||
|
if category not in prev_status:
|
||||||
|
out.add_heading(category + " -- Added")
|
||||||
|
BufferedOutput(with_lines=cur_lines).playback(out)
|
||||||
|
else:
|
||||||
|
# Actual comparison starts here...
|
||||||
|
prev_lines = prev_status[category]
|
||||||
|
def stringify(lines):
|
||||||
|
return [json.dumps(line) for line in lines]
|
||||||
|
diff = SequenceMatcher(None, stringify(prev_lines), stringify(cur_lines)).get_opcodes()
|
||||||
|
for op, i1, i2, j1, j2 in diff:
|
||||||
|
if op == "replace":
|
||||||
|
out.add_heading(category + " -- Previously:")
|
||||||
|
elif op == "delete":
|
||||||
|
out.add_heading(category + " -- Removed")
|
||||||
|
if op in ("replace", "delete"):
|
||||||
|
BufferedOutput(with_lines=prev_lines[i1:i2]).playback(out)
|
||||||
|
|
||||||
|
if op == "replace":
|
||||||
|
out.add_heading(category + " -- Currently:")
|
||||||
|
elif op == "insert":
|
||||||
|
out.add_heading(category + " -- Added")
|
||||||
|
if op in ("replace", "insert"):
|
||||||
|
BufferedOutput(with_lines=cur_lines[j1:j2]).playback(out)
|
||||||
|
|
||||||
|
for category, prev_lines in prev_status.items():
|
||||||
|
if category not in cur_status:
|
||||||
|
out.add_heading(category)
|
||||||
|
out.print_warning("This section was removed.")
|
||||||
|
|
||||||
|
if send_via_email:
|
||||||
|
# If there were changes, send off an email.
|
||||||
|
buf = out.buf.getvalue()
|
||||||
|
if len(buf) > 0:
|
||||||
|
# create MIME message
|
||||||
|
from email.message import Message
|
||||||
|
msg = Message()
|
||||||
|
msg['From'] = "\"%s\" <administrator@%s>" % (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'])
|
||||||
|
msg['To'] = "administrator@%s" % env['PRIMARY_HOSTNAME']
|
||||||
|
msg['Subject'] = "[%s] Status Checks Change Notice" % env['PRIMARY_HOSTNAME']
|
||||||
|
msg.set_payload(buf, "UTF-8")
|
||||||
|
|
||||||
|
# send to administrator@
|
||||||
|
import smtplib
|
||||||
|
mailserver = smtplib.SMTP('localhost', 25)
|
||||||
|
mailserver.ehlo()
|
||||||
|
mailserver.sendmail(
|
||||||
|
"administrator@%s" % env['PRIMARY_HOSTNAME'], # MAIL FROM
|
||||||
|
"administrator@%s" % env['PRIMARY_HOSTNAME'], # RCPT TO
|
||||||
|
msg.as_string())
|
||||||
|
mailserver.quit()
|
||||||
|
|
||||||
|
# Store the current status checks output for next time.
|
||||||
|
os.makedirs(os.path.dirname(cache_fn), exist_ok=True)
|
||||||
|
with open(cache_fn, "w") as f:
|
||||||
|
json.dump(cur.buf, f, indent=True)
|
||||||
|
|
||||||
|
class FileOutput:
|
||||||
|
def __init__(self, buf, width):
|
||||||
|
self.buf = buf
|
||||||
|
self.width = width
|
||||||
|
|
||||||
def add_heading(self, heading):
|
def add_heading(self, heading):
|
||||||
print()
|
print(file=self.buf)
|
||||||
print(heading)
|
print(heading, file=self.buf)
|
||||||
print("=" * len(heading))
|
print("=" * len(heading), file=self.buf)
|
||||||
|
|
||||||
def print_ok(self, message):
|
def print_ok(self, message):
|
||||||
self.print_block(message, first_line="✓ ")
|
self.print_block(message, first_line="✓ ")
|
||||||
@@ -743,28 +866,36 @@ class ConsoleOutput:
|
|||||||
self.print_block(message, first_line="? ")
|
self.print_block(message, first_line="? ")
|
||||||
|
|
||||||
def print_block(self, message, first_line=" "):
|
def print_block(self, message, first_line=" "):
|
||||||
print(first_line, end='')
|
print(first_line, end='', file=self.buf)
|
||||||
message = re.sub("\n\s*", " ", message)
|
message = re.sub("\n\s*", " ", message)
|
||||||
words = re.split("(\s+)", message)
|
words = re.split("(\s+)", message)
|
||||||
linelen = 0
|
linelen = 0
|
||||||
for w in words:
|
for w in words:
|
||||||
if linelen + len(w) > self.terminal_columns-1-len(first_line):
|
if linelen + len(w) > self.width-1-len(first_line):
|
||||||
print()
|
print(file=self.buf)
|
||||||
print(" ", end="")
|
print(" ", end="", file=self.buf)
|
||||||
linelen = 0
|
linelen = 0
|
||||||
if linelen == 0 and w.strip() == "": continue
|
if linelen == 0 and w.strip() == "": continue
|
||||||
print(w, end="")
|
print(w, end="", file=self.buf)
|
||||||
linelen += len(w)
|
linelen += len(w)
|
||||||
print()
|
print(file=self.buf)
|
||||||
|
|
||||||
def print_line(self, message, monospace=False):
|
def print_line(self, message, monospace=False):
|
||||||
for line in message.split("\n"):
|
for line in message.split("\n"):
|
||||||
self.print_block(line)
|
self.print_block(line)
|
||||||
|
|
||||||
|
class ConsoleOutput(FileOutput):
|
||||||
|
def __init__(self):
|
||||||
|
self.buf = sys.stdout
|
||||||
|
try:
|
||||||
|
self.width = int(shell('check_output', ['stty', 'size']).split()[1])
|
||||||
|
except:
|
||||||
|
self.width = 76
|
||||||
|
|
||||||
class BufferedOutput:
|
class BufferedOutput:
|
||||||
# Record all of the instance method calls so we can play them back later.
|
# Record all of the instance method calls so we can play them back later.
|
||||||
def __init__(self):
|
def __init__(self, with_lines=None):
|
||||||
self.buf = []
|
self.buf = [] if not with_lines else with_lines
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
if attr not in ("add_heading", "print_ok", "print_error", "print_warning", "print_block", "print_line"):
|
if attr not in ("add_heading", "print_ok", "print_error", "print_warning", "print_block", "print_line"):
|
||||||
raise AttributeError
|
raise AttributeError
|
||||||
@@ -778,12 +909,17 @@ class BufferedOutput:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
|
||||||
from utils import load_environment
|
from utils import load_environment
|
||||||
|
|
||||||
env = load_environment()
|
env = load_environment()
|
||||||
|
pool = multiprocessing.pool.Pool(processes=10)
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
pool = multiprocessing.pool.Pool(processes=10)
|
run_checks(False, env, ConsoleOutput(), pool)
|
||||||
run_checks(env, ConsoleOutput(), pool)
|
|
||||||
|
elif sys.argv[1] == "--show-changes":
|
||||||
|
run_and_output_changes(env, pool, sys.argv[-1] == "--smtp")
|
||||||
|
|
||||||
elif sys.argv[1] == "--check-primary-hostname":
|
elif sys.argv[1] == "--check-primary-hostname":
|
||||||
# See if the primary hostname appears resolvable and has a signed certificate.
|
# See if the primary hostname appears resolvable and has a signed certificate.
|
||||||
domain = env['PRIMARY_HOSTNAME']
|
domain = env['PRIMARY_HOSTNAME']
|
||||||
@@ -796,5 +932,3 @@ if __name__ == "__main__":
|
|||||||
if cert_status != "OK":
|
if cert_status != "OK":
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<option value="AAAA" data-hint="Enter an IPv6 address.">AAAA (IPv6 address)</option>
|
<option value="AAAA" data-hint="Enter an IPv6 address.">AAAA (IPv6 address)</option>
|
||||||
<option value="CNAME" data-hint="Enter another domain name followed by a period at the end (e.g. mypage.github.io.).">CNAME (DNS forwarding)</option>
|
<option value="CNAME" data-hint="Enter another domain name followed by a period at the end (e.g. mypage.github.io.).">CNAME (DNS forwarding)</option>
|
||||||
<option value="TXT" data-hint="Enter arbitrary text.">TXT (text record)</option>
|
<option value="TXT" data-hint="Enter arbitrary text.">TXT (text record)</option>
|
||||||
|
<option value="MX" data-hint="Enter record in the form of PRIORIY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">MX (mail exchanger)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ function api(url, method, data, callback, callback_error) {
|
|||||||
ajax({
|
ajax({
|
||||||
url: "/admin" + url,
|
url: "/admin" + url,
|
||||||
method: method,
|
method: method,
|
||||||
|
cache: false,
|
||||||
data: data,
|
data: data,
|
||||||
beforeSend: function(xhr) {
|
beforeSend: function(xhr) {
|
||||||
// We don't store user credentials in a cookie to avoid the hassle of CSRF
|
// We don't store user credentials in a cookie to avoid the hassle of CSRF
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<h3>Current Backups</h3>
|
<h3>Current Backups</h3>
|
||||||
|
|
||||||
<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>The backup directory currently contains the backups listed below. The total size on disk of the backups is currently <span id="backup-total-size"></span>.</p>
|
||||||
|
|
||||||
<table id="backup-status" class="table" style="width: auto">
|
<table id="backup-status" class="table" style="width: auto">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -76,7 +76,7 @@ function show_system_backup() {
|
|||||||
if (b.deleted_in)
|
if (b.deleted_in)
|
||||||
tr.append( $('<td/>').text(b.deleted_in) );
|
tr.append( $('<td/>').text(b.deleted_in) );
|
||||||
else
|
else
|
||||||
tr.append( $('<td class="text-muted">n/a</td>') );
|
tr.append( $('<td class="text-muted">unknown</td>') );
|
||||||
$('#backup-status tbody').append(tr);
|
$('#backup-status tbody').append(tr);
|
||||||
|
|
||||||
total_disk_size += b.size;
|
total_disk_size += b.size;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<h2>Users</h2>
|
<h2>Users</h2>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
#user_table h4 { margin: 1em 0 0 0; }
|
||||||
#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 div:before { content: "⇖ "; }
|
|
||||||
#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; }
|
||||||
|
#user_table .account_active.if_inactive { display: none; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<h3>Add a mail user</h3>
|
<h3>Add a mail user</h3>
|
||||||
@@ -77,11 +76,9 @@
|
|||||||
<td class='mailboxsize'>
|
<td class='mailboxsize'>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="user-extra-template">
|
<tr id="user-extra-template" class="if_inactive">
|
||||||
<td colspan="3" style="border-top: 0; padding-top: 0">
|
<td colspan="3" style="border: 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='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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -98,7 +95,7 @@ function show_users() {
|
|||||||
function(r) {
|
function(r) {
|
||||||
$('#user_table tbody').html("");
|
$('#user_table tbody').html("");
|
||||||
for (var i = 0; i < r.length; i++) {
|
for (var i = 0; i < r.length; i++) {
|
||||||
var hdr = $("<tr><td><h4/></td></tr>");
|
var hdr = $("<tr><td colspan='3'><h4/></td></tr>");
|
||||||
hdr.find('h4').text(r[i].domain);
|
hdr.find('h4').text(r[i].domain);
|
||||||
$('#user_table tbody').append(hdr);
|
$('#user_table tbody').append(hdr);
|
||||||
|
|
||||||
@@ -137,16 +134,6 @@ function show_users() {
|
|||||||
p.find('span.name').text(add_privs[j]);
|
p.find('span.name').text(add_privs[j]);
|
||||||
n.find('.add-privs').append(p);
|
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(", ") : "")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#########################################################
|
#########################################################
|
||||||
|
|
||||||
if [ -z "$TAG" ]; then
|
if [ -z "$TAG" ]; then
|
||||||
TAG=v0.07
|
TAG=v0.08
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Are we running as root?
|
# Are we running as root?
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ tools/editconf.py /etc/opendmarc.conf -s \
|
|||||||
#
|
#
|
||||||
# Be careful. If we add other milters later, this needs to be concatenated
|
# Be careful. If we add other milters later, this needs to be concatenated
|
||||||
# on the smtpd_milters line.
|
# on the smtpd_milters line.
|
||||||
|
#
|
||||||
|
# The OpenDMARC milter is skipped in the SMTP submission listener by
|
||||||
|
# configuring smtpd_milters there to only list the OpenDKIM milter
|
||||||
|
# (see mail-postfix.sh).
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
"smtpd_milters=inet:127.0.0.1:8891 inet:127.0.0.1:8893"\
|
"smtpd_milters=inet:127.0.0.1:8891 inet:127.0.0.1:8893"\
|
||||||
non_smtpd_milters=\$smtpd_milters \
|
non_smtpd_milters=\$smtpd_milters \
|
||||||
|
|||||||
@@ -179,3 +179,21 @@ function input_menu {
|
|||||||
result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 $3)
|
result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 $3)
|
||||||
result_code=$?
|
result_code=$?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function git_clone {
|
||||||
|
# Clones a git repository, checks out a particular commit or tag,
|
||||||
|
# and moves the repository (or a subdirectory in it) to some path.
|
||||||
|
# We use separate clone and checkout because -b only supports tags
|
||||||
|
# and branches, but we sometimes want to reference a commit hash
|
||||||
|
# directly when the repo doesn't provide a tag.
|
||||||
|
REPO=$1
|
||||||
|
TREEISH=$2
|
||||||
|
SUBDIR=$3
|
||||||
|
TARGETPATH=$4
|
||||||
|
TMPPATH=/tmp/git-clone-$$
|
||||||
|
rm -rf $TMPPATH $TARGETPATH
|
||||||
|
git clone -q $REPO $TMPPATH || exit 1
|
||||||
|
(cd $TMPPATH; git checkout -q $TREEISH;) || exit 1
|
||||||
|
mv $TMPPATH/$SUBDIR $TARGETPATH
|
||||||
|
rm -rf $TMPPATH
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||||||
|
|
||||||
# Enable the 'submission' port 587 smtpd server and tweak its settings.
|
# Enable the 'submission' port 587 smtpd server and tweak its settings.
|
||||||
#
|
#
|
||||||
|
# * Do not add the OpenDMAC Authentication-Results header. That should only be added
|
||||||
|
# on incoming mail. Omit the OpenDMARC milter by re-setting smtpd_milters to the
|
||||||
|
# OpenDKIM milter only. See dkim.sh.
|
||||||
# * Require the best ciphers for incoming connections per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
|
# * Require the best ciphers for incoming connections per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
|
||||||
# By putting this setting here we leave opportunistic TLS on incoming mail at default cipher settings (any cipher is better than none).
|
# By putting this setting here we leave opportunistic TLS on incoming mail at default cipher settings (any cipher is better than none).
|
||||||
# * Give it a different name in syslog to distinguish it from the port 25 smtpd server.
|
# * Give it a different name in syslog to distinguish it from the port 25 smtpd server.
|
||||||
@@ -71,6 +74,7 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||||||
tools/editconf.py /etc/postfix/master.cf -s -w \
|
tools/editconf.py /etc/postfix/master.cf -s -w \
|
||||||
"submission=inet n - - - - smtpd
|
"submission=inet n - - - - smtpd
|
||||||
-o syslog_name=postfix/submission
|
-o syslog_name=postfix/submission
|
||||||
|
-o smtpd_milters=inet:127.0.0.1:8891
|
||||||
-o smtpd_tls_ciphers=high -o smtpd_tls_protocols=!SSLv2,!SSLv3
|
-o smtpd_tls_ciphers=high -o smtpd_tls_protocols=!SSLv2,!SSLv3
|
||||||
-o cleanup_service_name=authclean" \
|
-o cleanup_service_name=authclean" \
|
||||||
"authclean=unix n - - - 0 cleanup
|
"authclean=unix n - - - 0 cleanup
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ $(pwd)/management/backup.py
|
|||||||
EOF
|
EOF
|
||||||
chmod +x /etc/cron.daily/mailinabox-backup
|
chmod +x /etc/cron.daily/mailinabox-backup
|
||||||
|
|
||||||
|
# Perform daily status checks. Compare each day to the previous
|
||||||
|
# for changes and mail the changes to the administrator.
|
||||||
|
cat > /etc/cron.daily/mailinabox-statuschecks << EOF;
|
||||||
|
#!/bin/bash
|
||||||
|
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
|
||||||
|
# Run status checks.
|
||||||
|
$(pwd)/management/status_checks.py --show-changes --smtp
|
||||||
|
EOF
|
||||||
|
chmod +x /etc/cron.daily/mailinabox-statuschecks
|
||||||
|
|
||||||
|
|
||||||
# Start it. Remove the api key file first so that start.sh
|
# Start it. Remove the api key file first so that start.sh
|
||||||
# can wait for it to be created to know that the management
|
# can wait for it to be created to know that the management
|
||||||
# server is ready.
|
# server is ready.
|
||||||
|
|||||||
@@ -84,13 +84,22 @@ def run_migrations():
|
|||||||
env = load_environment()
|
env = load_environment()
|
||||||
|
|
||||||
migration_id_file = os.path.join(env['STORAGE_ROOT'], 'mailinabox.version')
|
migration_id_file = os.path.join(env['STORAGE_ROOT'], 'mailinabox.version')
|
||||||
|
migration_id = None
|
||||||
if os.path.exists(migration_id_file):
|
if os.path.exists(migration_id_file):
|
||||||
with open(migration_id_file) as f:
|
with open(migration_id_file) as f:
|
||||||
ourver = int(f.read().strip())
|
migration_id = f.read().strip();
|
||||||
else:
|
|
||||||
|
if migration_id is None:
|
||||||
# Load the legacy location of the migration ID. We'll drop support
|
# Load the legacy location of the migration ID. We'll drop support
|
||||||
# for this eventually.
|
# for this eventually.
|
||||||
ourver = int(env.get("MIGRATIONID", "0"))
|
migration_id = env.get("MIGRATIONID")
|
||||||
|
|
||||||
|
if migration_id is None:
|
||||||
|
print()
|
||||||
|
print("%s file doesn't exists. Skipping migration..." % (migration_id_file,))
|
||||||
|
return
|
||||||
|
|
||||||
|
ourver = int(migration_id)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
next_ver = (ourver + 1)
|
next_ver = (ourver + 1)
|
||||||
|
|||||||
@@ -15,18 +15,49 @@ apt_install \
|
|||||||
apt-get purge -qq -y owncloud*
|
apt-get purge -qq -y owncloud*
|
||||||
|
|
||||||
# Install ownCloud from source of this version:
|
# Install ownCloud from source of this version:
|
||||||
owncloud_ver=7.0.4
|
owncloud_ver=8.0.2
|
||||||
|
|
||||||
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
|
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
|
||||||
if [ ! -d /usr/local/lib/owncloud/ ] \
|
if [ ! -d /usr/local/lib/owncloud/ ] \
|
||||||
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
|
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
|
||||||
|
|
||||||
echo installing ownCloud...
|
# Clear out the existing ownCloud.
|
||||||
|
rm -f /tmp/owncloud-config.php
|
||||||
|
if [ ! -d /usr/local/lib/owncloud/ ]; then
|
||||||
|
echo installing ownCloud...
|
||||||
|
else
|
||||||
|
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud directory to /tmp/owncloud-backup-$$)..."
|
||||||
|
cp /usr/local/lib/owncloud/config/config.php /tmp/owncloud-config.php
|
||||||
|
mv /usr/local/lib/owncloud /tmp/owncloud-backup-$$
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download and extract ownCloud.
|
||||||
rm -f /tmp/owncloud.zip
|
rm -f /tmp/owncloud.zip
|
||||||
wget -qO /tmp/owncloud.zip https://download.owncloud.org/community/owncloud-$owncloud_ver.zip
|
wget -qO /tmp/owncloud.zip https://download.owncloud.org/community/owncloud-$owncloud_ver.zip
|
||||||
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
|
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
|
||||||
hide_output php /usr/local/lib/owncloud/occ upgrade #if OC is up-to-date it wont matter
|
|
||||||
rm -f /tmp/owncloud.zip
|
rm -f /tmp/owncloud.zip
|
||||||
|
|
||||||
|
# The two apps we actually want are not in ownCloud core. Clone them from
|
||||||
|
# their github repositories.
|
||||||
|
mkdir -p /usr/local/lib/owncloud/apps
|
||||||
|
git_clone https://github.com/owncloud/contacts v$owncloud_ver '' /usr/local/lib/owncloud/apps/contacts
|
||||||
|
git_clone https://github.com/owncloud/calendar v$owncloud_ver '' /usr/local/lib/owncloud/apps/calendar
|
||||||
|
|
||||||
|
# Fix weird permissions.
|
||||||
|
chmod 750 /usr/local/lib/owncloud/{apps,config}
|
||||||
|
|
||||||
|
# Restore configuration file if we're doing an upgrade.
|
||||||
|
if [ -f /tmp/owncloud-config.php ]; then
|
||||||
|
mv /tmp/owncloud-config.php /usr/local/lib/owncloud/config/config.php
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure permissions are correct or the upgrade step won't run.
|
||||||
|
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
|
||||||
|
# that error.
|
||||||
|
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
|
||||||
|
|
||||||
|
# Run the upgrade script (if ownCloud is already up-to-date it wont matter).
|
||||||
|
hide_output sudo -u www-data php /usr/local/lib/owncloud/occ upgrade
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ### Configuring ownCloud
|
# ### Configuring ownCloud
|
||||||
@@ -105,9 +136,12 @@ fi
|
|||||||
|
|
||||||
# Enable/disable apps. Note that this must be done after the ownCloud setup.
|
# Enable/disable apps. Note that this must be done after the ownCloud setup.
|
||||||
# The firstrunwizard gave Josh all sorts of problems, so disabling that.
|
# The firstrunwizard gave Josh all sorts of problems, so disabling that.
|
||||||
# user_external is what allows ownCloud to use IMAP for login.
|
# user_external is what allows ownCloud to use IMAP for login. The contacts
|
||||||
hide_output php /usr/local/lib/owncloud/console.php app:disable firstrunwizard
|
# and calendar apps are the extensions we really care about here.
|
||||||
hide_output php /usr/local/lib/owncloud/console.php app:enable user_external
|
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable firstrunwizard
|
||||||
|
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable user_external
|
||||||
|
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable contacts
|
||||||
|
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable calendar
|
||||||
|
|
||||||
# Set PHP FPM values to support large file uploads
|
# Set PHP FPM values to support large file uploads
|
||||||
# (semicolon is the comment character in this file, hashes produce deprecation warnings)
|
# (semicolon is the comment character in this file, hashes produce deprecation warnings)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ fi
|
|||||||
if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
|
if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
|
||||||
echo "Private IPv6 Address: $PRIVATE_IPV6"
|
echo "Private IPv6 Address: $PRIVATE_IPV6"
|
||||||
fi
|
fi
|
||||||
if [ -f /usr/bin/git ]; then
|
if [ -f .git ]; then
|
||||||
echo "Mail-in-a-Box Version: " $(git describe)
|
echo "Mail-in-a-Box Version: " $(git describe)
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
@@ -109,6 +109,10 @@ fi
|
|||||||
# Create the STORAGE_ROOT if it not exists
|
# Create the STORAGE_ROOT if it not exists
|
||||||
if [ ! -d $STORAGE_ROOT ]; then
|
if [ ! -d $STORAGE_ROOT ]; then
|
||||||
mkdir -p $STORAGE_ROOT
|
mkdir -p $STORAGE_ROOT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create mailinabox.version file if not exists
|
||||||
|
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
|
||||||
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
|
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
|
||||||
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
|
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ apt_get_quiet upgrade
|
|||||||
# * cron: Runs background processes periodically.
|
# * cron: Runs background processes periodically.
|
||||||
# * ntp: keeps the system time correct
|
# * ntp: keeps the system time correct
|
||||||
# * fail2ban: scans log files for repeated failed login attempts and blocks the remote IP at the firewall
|
# * fail2ban: scans log files for repeated failed login attempts and blocks the remote IP at the firewall
|
||||||
|
# * git: we install some things directly from github
|
||||||
# * sudo: allows privileged users to execute commands as root without being root
|
# * sudo: allows privileged users to execute commands as root without being root
|
||||||
# * coreutils: includes `nproc` tool to report number of processors
|
# * coreutils: includes `nproc` tool to report number of processors
|
||||||
# * bc: allows us to do math to compute sane defaults
|
# * bc: allows us to do math to compute sane defaults
|
||||||
|
|
||||||
apt_install python3 python3-dev python3-pip \
|
apt_install python3 python3-dev python3-pip \
|
||||||
wget curl sudo coreutils bc \
|
wget curl git sudo coreutils bc \
|
||||||
haveged unattended-upgrades cron ntp fail2ban
|
haveged unattended-upgrades cron ntp fail2ban
|
||||||
|
|
||||||
# Allow apt to install system updates automatically every day.
|
# Allow apt to install system updates automatically every day.
|
||||||
@@ -106,3 +107,11 @@ fi
|
|||||||
|
|
||||||
restart_service bind9
|
restart_service bind9
|
||||||
restart_service resolvconf
|
restart_service resolvconf
|
||||||
|
|
||||||
|
# ### Fail2Ban Service
|
||||||
|
|
||||||
|
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix and ssh
|
||||||
|
cp conf/fail2ban/jail.local /etc/fail2ban/jail.local
|
||||||
|
cp conf/fail2ban/dovecotimap.conf /etc/fail2ban/filter.d/dovecotimap.conf
|
||||||
|
|
||||||
|
restart_service fail2ban
|
||||||
|
|||||||
@@ -30,24 +30,33 @@ apt_install \
|
|||||||
apt-get purge -qq -y roundcube* #NODOC
|
apt-get purge -qq -y roundcube* #NODOC
|
||||||
|
|
||||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||||
|
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
|
||||||
|
# whether we have the latest version.
|
||||||
VERSION=1.1.0
|
VERSION=1.1.0
|
||||||
|
VACATION_SIEVE_VERSION=06a20e9d44db62259ae41fd8451f3c937d3ab4f3
|
||||||
needs_update=0 #NODOC
|
needs_update=0 #NODOC
|
||||||
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
||||||
# not installed yet #NODOC
|
# not installed yet #NODOC
|
||||||
needs_update=1 #NODOC
|
needs_update=1 #NODOC
|
||||||
elif [[ $VERSION != `cat /usr/local/lib/roundcubemail/version` ]]; then
|
elif [[ "$VERSION:$VACATION_SIEVE_VERSION" != `cat /usr/local/lib/roundcubemail/version` ]]; then
|
||||||
# checks if the version is what we want
|
# checks if the version is what we want
|
||||||
needs_update=1 #NODOC
|
needs_update=1 #NODOC
|
||||||
fi
|
fi
|
||||||
if [ $needs_update == 1 ]; then
|
if [ $needs_update == 1 ]; then
|
||||||
echo installing roundcube webmail $VERSION...
|
# install roundcube
|
||||||
|
echo installing Roundcube webmail $VERSION...
|
||||||
rm -f /tmp/roundcube.tgz
|
rm -f /tmp/roundcube.tgz
|
||||||
wget -qO /tmp/roundcube.tgz http://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz
|
wget -qO /tmp/roundcube.tgz http://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz
|
||||||
tar -C /usr/local/lib -zxf /tmp/roundcube.tgz
|
tar -C /usr/local/lib -zxf /tmp/roundcube.tgz
|
||||||
rm -rf /usr/local/lib/roundcubemail
|
rm -rf /usr/local/lib/roundcubemail
|
||||||
mv /usr/local/lib/roundcubemail-$VERSION/ /usr/local/lib/roundcubemail
|
mv /usr/local/lib/roundcubemail-$VERSION/ /usr/local/lib/roundcubemail
|
||||||
rm -f /tmp/roundcube.tgz
|
rm -f /tmp/roundcube.tgz
|
||||||
echo $VERSION > /usr/local/lib/roundcubemail/version
|
|
||||||
|
# install roundcube autoreply/vacation plugin
|
||||||
|
git_clone https://github.com/arodier/Roundcube-Plugins.git $VACATION_SIEVE_VERSION plugins/vacation_sieve /usr/local/lib/roundcubemail/plugins/vacation_sieve
|
||||||
|
|
||||||
|
# record the version we've installed
|
||||||
|
echo $VERSION:$VACATION_SIEVE_VERSION > /usr/local/lib/roundcubemail/version
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ### Configuring Roundcube
|
# ### Configuring Roundcube
|
||||||
@@ -79,7 +88,7 @@ cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
|
|||||||
\$config['support_url'] = 'https://mailinabox.email/';
|
\$config['support_url'] = 'https://mailinabox.email/';
|
||||||
\$config['product_name'] = 'Mail-in-a-Box/Roundcube Webmail';
|
\$config['product_name'] = 'Mail-in-a-Box/Roundcube Webmail';
|
||||||
\$config['des_key'] = '$SECRET_KEY';
|
\$config['des_key'] = '$SECRET_KEY';
|
||||||
\$config['plugins'] = array('archive', 'zipdownload', 'password', 'managesieve');
|
\$config['plugins'] = array('archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'vacation_sieve');
|
||||||
\$config['skin'] = 'classic';
|
\$config['skin'] = 'classic';
|
||||||
\$config['login_autocomplete'] = 2;
|
\$config['login_autocomplete'] = 2;
|
||||||
\$config['password_charset'] = 'UTF-8';
|
\$config['password_charset'] = 'UTF-8';
|
||||||
@@ -87,6 +96,26 @@ cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
|
|||||||
?>
|
?>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Configure vaction_sieve.
|
||||||
|
cat > /usr/local/lib/roundcubemail/plugins/vacation_sieve/config.inc.php <<EOF;
|
||||||
|
<?php
|
||||||
|
/* Do not edit. Written by Mail-in-a-Box. Regenerated on updates. */
|
||||||
|
\$rcmail_config['vacation_sieve'] = array(
|
||||||
|
'date_format' => 'd/m/Y',
|
||||||
|
'working_hours' => array(8,18),
|
||||||
|
'msg_format' => 'text',
|
||||||
|
'logon_transform' => array('#([a-z])[a-z]+(\.|\s)([a-z])#i', '\$1\$3'),
|
||||||
|
'transfer' => array(
|
||||||
|
'mode' => 'managesieve',
|
||||||
|
'ms_activate_script' => true,
|
||||||
|
'host' => 'localhost',
|
||||||
|
'port' => '4190',
|
||||||
|
'usetls' => false,
|
||||||
|
'path' => 'vacation',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
EOF
|
||||||
|
|
||||||
# Create writable directories.
|
# Create writable directories.
|
||||||
mkdir -p /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
mkdir -p /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
||||||
chown -R www-data.www-data /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
chown -R www-data.www-data /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
|
||||||
|
|||||||
@@ -30,17 +30,11 @@ elif [[ $TARGETHASH != `cat /usr/local/lib/z-push/version` ]]; then
|
|||||||
needs_update=1 #NODOC
|
needs_update=1 #NODOC
|
||||||
fi
|
fi
|
||||||
if [ $needs_update == 1 ]; then
|
if [ $needs_update == 1 ]; then
|
||||||
rm -rf /usr/local/lib/z-push
|
|
||||||
rm -f /tmp/zpush-repo
|
|
||||||
echo installing z-push \(fmbiete fork\)...
|
echo installing z-push \(fmbiete fork\)...
|
||||||
git clone -q https://github.com/fmbiete/Z-Push-contrib /tmp/zpush-repo
|
git_clone https://github.com/fmbiete/Z-Push-contrib $TARGETHASH '' /usr/local/lib/z-push
|
||||||
(cd /tmp/zpush-repo/; git checkout -q $TARGETHASH;)
|
|
||||||
rm -rf /tmp/zpush-repo/.git
|
|
||||||
mv /tmp/zpush-repo /usr/local/lib/z-push
|
|
||||||
rm -f /usr/sbin/z-push-{admin,top}
|
rm -f /usr/sbin/z-push-{admin,top}
|
||||||
ln -s /usr/local/lib/z-push/z-push-admin.php /usr/sbin/z-push-admin
|
ln -s /usr/local/lib/z-push/z-push-admin.php /usr/sbin/z-push-admin
|
||||||
ln -s /usr/local/lib/z-push/z-push-top.php /usr/sbin/z-push-top
|
ln -s /usr/local/lib/z-push/z-push-top.php /usr/sbin/z-push-top
|
||||||
rm -f /tmp/zpush-repo
|
|
||||||
echo $TARGETHASH > /usr/local/lib/z-push/version
|
echo $TARGETHASH > /usr/local/lib/z-push/version
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,17 @@ def mgmt(cmd, data=None, is_json=False):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
def read_password():
|
def read_password():
|
||||||
first = getpass.getpass('password: ')
|
while True:
|
||||||
second = getpass.getpass(' (again): ')
|
first = getpass.getpass('password: ')
|
||||||
while first != second:
|
if len(first) < 4:
|
||||||
print('Passwords not the same. Try again.')
|
print('Passwords must be at least four characters.')
|
||||||
first = getpass.getpass('password: ')
|
continue
|
||||||
second = getpass.getpass(' (again): ')
|
second = getpass.getpass(' (again): ')
|
||||||
return first
|
if first != second:
|
||||||
|
print('Passwords not the same. Try again.')
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
return first
|
||||||
|
|
||||||
def setup_key_auth(mgmt_uri):
|
def setup_key_auth(mgmt_uri):
|
||||||
key = open('/var/lib/mailinabox/api.key').read().strip()
|
key = open('/var/lib/mailinabox/api.key').read().strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user