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

Compare commits

...

38 Commits
2fa ... v0.07

Author SHA1 Message Date
Joshua Tauberer
1be0f39be0 prep for v0.07 tag 2015-02-28 17:09:12 -05:00
Joshua Tauberer
d01001f2a5 some more CHANGELOG entries 2015-02-28 17:06:09 -05:00
Joshua Tauberer
7c85694d60 Merge pull request #332 from mathuin/better-mx-check
Changed MX check to respect priorities other than 10.
2015-02-23 07:56:24 -05:00
Jack Twilley
b2fcd4c9e5 Now supports domains with multiple MX records.
The status check on MX records now correctly handles domains with
multiple MX records.
2015-02-22 17:05:09 -08:00
Jack Twilley
ead6f96513 Changed MX check to respect priorities other than 10.
Reordered the if a little, added some string parsing, and modified the
OK text to include a warning.
2015-02-20 11:29:28 -08:00
Joshua Tauberer
7ec662c83f status checks: use a worker pool that lives across flask requests, see #327 2015-02-18 16:42:33 +00:00
Joshua Tauberer
348d2b8701 Merge pull request #326 from dhpiggott/custom-dns-filter-secondary-nameserver
Do not show '_secondary_nameserver' in Custom DNS table
2015-02-17 08:31:34 -05:00
David Piggott
12f0dcb23b Do not show '_secondary_nameserver' in Custom DNS table
It's redundant and potentially confusing, as any secondary NS shows in "Using a
Secondary Nameserver".
2015-02-17 13:28:48 +00:00
Joshua Tauberer
449a538e6b if a CNAME is set for a domain, don't create a website for that domain (just like A/AAAA records) 2015-02-17 00:48:26 +00:00
Joshua Tauberer
3c50c9a18b when serving a 'www.' domain, check if the parent domain's ssl certificate can be used besides checking PRIMARY_HOSTNAME
Removing buy_certificate.py which is not working and I don't want to update its call signatures.
2015-02-17 00:42:25 +00:00
Joshua Tauberer
3c10ec70a5 update comment 2015-02-17 00:08:04 +00:00
Joshua Tauberer
1a59f343c0 adding entries to the CHANGELOG 2015-02-16 23:58:17 +00:00
Joshua Tauberer
fba4d4702e install opendmarc to add Authentication-Results headers for DMARC too 2015-02-16 23:17:44 +00:00
Joshua Tauberer
143bbf37f4 all mail domains, not just (top-level) zones, must have an entry in the opendkim key tables so that such outgoing mail gets signed
If you had both x.y.com and y.com configured here, x.y.com mail would not get DKIM-signed.
2015-02-16 18:13:51 -05:00
Joshua Tauberer
fd3ad267ba if a domain has a catch-all or domain alias then we no longer force the creation of postmaster@ and so we should not be checking for its existence in the status checks
see 85a40da83c
2015-02-15 19:07:10 -05:00
Joshua Tauberer
330583f71d status checks: if a service isn't available publicly, check if it is available on the loopback interface to distinguish not running from not accessible 2015-02-13 09:30:25 -05:00
Joshua Tauberer
d775f90f0c prevent apt from asking the user any questions
Add additional options to really prevent apt from asking questions, which causes setup to hang because stdin/out have been redirected.

fixes #270, #291
2015-02-13 13:41:52 +00:00
Joshua Tauberer
e096144713 Outlook 2007 or later on Windows 7 and later
fixes #308
2015-02-13 13:29:01 +00:00
Joshua Tauberer
7ce30ba888 roundcube 1.1.0 2015-02-13 13:22:46 +00:00
Joshua Tauberer
6a3ec1d874 updating CHANGELOG 2015-02-13 13:20:55 +00:00
Joshua Tauberer
575d3a66c6 more on being smarter about waiting for the management daemon to start
cc333b3965 worked for fresh systems, but if the system already had the daemon running the api.key file would already exist and the test would pass to early. Now removing the file first.

fixes #322
2015-02-13 13:11:03 +00:00
Joshua Tauberer
cc333b3965 be smarter about waiting for the management daemon to start before accessing it 2015-02-10 10:03:07 -05:00
Joshua Tauberer
351758b3bd typo
typo in "roudcube"
2015-02-10 09:27:36 -05:00
Joshua Tauberer
94053d8432 Merge pull request #317 from bizonix/master
Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
2015-02-09 12:53:32 -05:00
BiZoNiX
e14b2826e0 Disable viewing dotfiles (.htaccess, .svn, .git, etc.) 2015-02-09 19:41:42 +02:00
Joshua Tauberer
150611123a typo/text tweak 2015-02-05 09:17:48 -05:00
Joshua Tauberer
abfc17ee62 web admin: simplify the instructions for creating a separate web directory for particular sites by moving it into a modal 2015-02-05 09:12:55 -05:00
Joshua Tauberer
97be9c94b9 if the user has set a http proxy or redirect on the root path of a domain, using custom.yaml, skip the domain from the static hosting panel because it wont be serving any static files 2015-02-05 08:55:57 -05:00
Joshua Tauberer
21b00e8fbb if a custom A record is set, dont put in a default AAAA record pointing to the box because it will probably be wrong --- the user should either set an AAAA record or let the domain not resolve on IPv6 2015-02-03 21:51:19 -05:00
Joshua Tauberer
01636c2e4b Merge branch 'h8h-master'
I squashed some commits together and modified the commit message...
2015-02-03 23:54:17 +00:00
H8H
005315cd29 removed hardcoded /home directory to apply the existing configuration options for STORAGE_USER/ROOT if they exist
Highest priority: the pre set STORAGE_ROOT/USER, midmost priority: the config settings, lowest priority: the default one.

fixes #309; closes #311
2015-02-03 23:52:02 +00:00
Ian Beringer
20d20df829 allow for non-standard ssh port in status check
closes #313
2015-02-01 23:06:56 +00:00
Joshua Tauberer
f945a1bc6b Merge pull request #312 from ikarus23/master
hide nginx version an OS information for better privacy
2015-02-01 14:25:39 -05:00
ikarus
3a09b04786 hide nginx version an OS information for better privacy. 2015-02-01 20:13:03 +01:00
Joshua Tauberer
82e752395b Merge pull request #310 from ikarus23/master
do better redirection from http to https
2015-01-31 19:58:31 -05:00
ikarus
e330abd587 do better redirection from http to https
Redirect using the 'return' directive and the built-in
variable '$request_uri' to avoid any capturing, matching
or evaluation of regular expressions.

It's best practice. See: http://wiki.nginx.org/Pitfalls#Taxing_Rewrites
2015-02-01 01:32:07 +01:00
Joshua Tauberer
16422b4055 adding items to the CHANGELOG 2015-01-31 21:36:37 +00:00
Joshua Tauberer
b9ca74c915 implement Mozilla (e.g. Thunderbird) autoconfiguration file
fixes #241
2015-01-31 21:33:18 +00:00
20 changed files with 317 additions and 282 deletions

View File

@@ -1,24 +1,46 @@
CHANGELOG CHANGELOG
========= =========
Development v0.07 (February 28, 2015)
----------- -------------------------
Mail:
* If the box manages mail for a domain and a subdomain of that domain, outbound mail from the subdomain was not DKIM-signed and would therefore fail DMARC tests on the receiving end, possibly result in the mail heading into spam folders.
* Auto-configuration for Mozilla Thunderbird, Evolution, KMail, and Kontact is now available.
* Domains that only have a catch-all alias or domain alias no longer automatically create/require admin@ and postmaster@ addresses since they'll forward anyway.
* Roundcube is updated to version 1.1.0.
* Authentication-Results headers for DMARC are now added to incoming mail.
DNS:
* If a custom CNAME record is set on a 'www' subdomain, the default A/AAAA records were preventing the CNAME from working.
* If a custom DNS A record overrides one provided by the box, the a corresponding default IPv6 record by the box is removed since it will probably be incorrect.
* Internationalized domain names (IDNs) are now supported for DNS and web, but email is not yet tested.
Web:
* Static websites now deny access to certain dot (.) files and directories which typically have sensitive info: .ht*, .svn*, .git*, .hg*, .bzr*.
* The nginx server no longer reports its version and OS for better privacy.
* The HTTP->HTTPS redirect is now more efficient.
* When serving a 'www.' domain, reuse the SSL certificate for the parent domain if it covers the 'www' subdomain too
* If a custom DNS CNAME record is set on a domain, don't offer to put a website on that domain. (Same logic already applies to custom A/AAAA records.)
Control panel: Control panel:
* Status checks now check that system services are actually running by pinging each port that should have something running on it. * Status checks now check that system services are actually running by pinging each port that should have something running on it.
* If a custom CNAME record is set on a 'www' subdomain, the default A/AAAA records were preventing the CNAME from working. * The status checks are now parallelized so they may be a little faster.
* The status check for MX records now allow any priority, in case an unusual setup is required.
* The interface for setting website domain-specific directories is simplified.
* The mail guide now says that to use Outlook, Outlook 2007 or later on Windows 7 and later is required.
* External DNS settings now skip the special "_secondary_nameserver" key which is used for storing secondary NS information.
Setup: Setup:
* Install cron if it isn't already installed. * Install cron if it isn't already installed.
* Fix a units problem in the minimum memory check. * Fix a units problem in the minimum memory check.
* If you override the STORAGE_ROOT, your setting will now persist if you re-run setup.
Miscellaneous: * Hangs due to apt wanting the user to resolve a conflict should now be fixed (apt will just clobber the problematic file now).
* Internationalized domain names (IDNs) are now supported for DNS and web, but email is not yet tested.
* Domains that only have a catch-all alias or domain alias no longer automatically create/require admin@ and postmaster@ addresses since they'll forward anyway.
v0.06 (January 4, 2015) v0.06 (January 4, 2015)
----------------------- -----------------------

View File

@@ -0,0 +1,44 @@
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="PRIMARY_HOSTNAME">
<domain>PRIMARY_HOSTNAME</domain>
<displayName>PRIMARY_HOSTNAME (Mail-in-a-Box)</displayName>
<displayShortName>PRIMARY_HOSTNAME</displayShortName>
<incomingServer type="imap">
<hostname>PRIMARY_HOSTNAME</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</incomingServer>
<outgoingServer type="smtp">
<hostname>PRIMARY_HOSTNAME</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</outgoingServer>
<documentation url="https://PRIMARY_HOSTNAME/">
<descr lang="en">PRIMARY_HOSTNAME website.</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url="https://PRIMARY_HOSTNAME/mail/" />
<loginPageInfo url="https://PRIMARY_HOSTNAME/mail/" >
<username>%EMAILADDRESS%</username>
<usernameField id="rcmloginuser" name="_user" />
<passwordField id="rcmloginpwd" name="_pass" />
<loginButton id="rcmloginsubmit" />
</loginPageInfo>
</webMail>
<clientConfigUpdate url="https://PRIMARY_HOSTNAME/.well-known/autoconfig/mail/config-v1.1.xml" />
</clientConfig>

View File

@@ -7,7 +7,15 @@ server {
server_name $HOSTNAME; server_name $HOSTNAME;
root /tmp/invalid-path-nothing-here; root /tmp/invalid-path-nothing-here;
rewrite ^/(.*)$ https://$HOSTNAME/$1 permanent;
# Improve privacy: Hide version an OS information on
# error pages and in the "Server" HTTP-Header.
server_tokens off;
# Redirect using the 'return' directive and the built-in
# variable '$request_uri' to avoid any capturing, matching
# or evaluation of regular expressions.
return 301 https://$HOSTNAME$request_uri;
} }
# The secure HTTPS server. # The secure HTTPS server.
@@ -17,6 +25,10 @@ server {
server_name $HOSTNAME; server_name $HOSTNAME;
# Improve privacy: Hide version an OS information on
# error pages and in the "Server" HTTP-Header.
server_tokens off;
ssl_certificate $SSL_CERTIFICATE; ssl_certificate $SSL_CERTIFICATE;
ssl_certificate_key $SSL_KEY; ssl_certificate_key $SSL_KEY;
include /etc/nginx/nginx-ssl.conf; include /etc/nginx/nginx-ssl.conf;
@@ -38,6 +50,16 @@ server {
location = /mailinabox.mobileconfig { location = /mailinabox.mobileconfig {
alias /var/lib/mailinabox/mobileconfig.xml; alias /var/lib/mailinabox/mobileconfig.xml;
} }
location = /.well-known/autoconfig/mail/config-v1.1.xml {
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
}
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
location ~ /\.(ht|svn|git|hg|bzr) {
log_not_found off;
access_log off;
deny all;
}
# Roundcube Webmail configuration. # Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect; rewrite ^/mail$ /mail/ redirect;

View File

@@ -1,155 +0,0 @@
#!/usr/bin/python3
# Helps you purchase a SSL certificate from Gandi.net using
# their API.
#
# Before you begin:
# 1) Create an account on Gandi.net.
# 2) Pre-pay $16 into your account at https://www.gandi.net/prepaid/operations. Wait until the payment goes through.
# 3) Activate your API key first on the test platform (wait a while, refresh the page) and then activate the production API at https://www.gandi.net/admin/api_key.
import sys, re, os.path, urllib.request
import xmlrpc.client
import rtyaml
from utils import load_environment, shell
from web_update import get_web_domains, get_domain_ssl_files, get_web_root
from status_checks import check_certificate
def buy_ssl_certificate(api_key, domain, command, env):
if domain != env['PRIMARY_HOSTNAME'] \
and domain not in get_web_domains(env):
raise ValueError("Domain is not %s or a domain we're serving a website for." % env['PRIMARY_HOSTNAME'])
# Initialize.
gandi = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')
try:
existing_certs = gandi.cert.list(api_key)
except Exception as e:
if "Invalid API key" in str(e):
print("Invalid API key. Check that you copied the API Key correctly from https://www.gandi.net/admin/api_key.")
sys.exit(1)
else:
raise
# Where is the SSL cert stored?
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env)
# Have we already created a cert for this domain?
for cert in existing_certs:
if cert['cn'] == domain:
break
else:
# No existing cert found. Purchase one.
if command != 'purchase':
print("No certificate or order found yet. If you haven't yet purchased a certificate, run ths script again with the 'purchase' command. Otherwise wait a moment and try again.")
sys.exit(1)
else:
# Start an order for a single standard SSL certificate.
# Use DNS validation. Web-based validation won't work because they
# require a file on HTTP but not HTTPS w/o redirects and we don't
# serve anything plainly over HTTP. Email might be another way but
# DNS is easier to automate.
op = gandi.cert.create(api_key, {
"csr": open(ssl_csr_path).read(),
"dcv_method": "dns",
"duration": 1, # year?
"package": "cert_std_1_0_0",
})
print("An SSL certificate has been ordered.")
print()
print(op)
print()
print("In a moment please run this script again with the 'setup' command.")
if cert['status'] == 'pending':
# Get the information we need to update our DNS with a code so that
# Gandi can verify that we own the domain.
dcv = gandi.cert.get_dcv_params(api_key, {
"csr": open(ssl_csr_path).read(),
"cert_id": cert['id'],
"dcv_method": "dns",
"duration": 1, # year?
"package": "cert_std_1_0_0",
})
if dcv["dcv_method"] != "dns":
raise Exception("Certificate ordered with an unknown validation method.")
# Update our DNS data.
dns_config = env['STORAGE_ROOT'] + '/dns/custom.yaml'
if os.path.exists(dns_config):
dns_records = rtyaml.load(open(dns_config))
else:
dns_records = { }
qname = dcv['md5'] + '.' + domain
value = dcv['sha1'] + '.comodoca.com.'
dns_records[qname] = { "CNAME": value }
with open(dns_config, 'w') as f:
f.write(rtyaml.dump(dns_records))
shell('check_call', ['tools/dns_update'])
# Okay, done with this step.
print("DNS has been updated. Gandi will check within 60 minutes.")
print()
print("See https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
elif cert['status'] == 'valid':
# The certificate is ready.
# Check before we overwrite something we shouldn't.
if os.path.exists(ssl_certificate):
cert_status, cert_status_details = check_certificate(None, ssl_certificate, None)
if cert_status != "SELF-SIGNED":
print("Please back up and delete the file %s so I can save your new certificate." % ssl_certificate)
sys.exit(1)
# Form the certificate.
# The certificate comes as a long base64-encoded string. Break in
# into lines in the usual way.
pem = "-----BEGIN CERTIFICATE-----\n"
pem += "\n".join(chunk for chunk in re.split(r"(.{64})", cert['cert']) if chunk != "")
pem += "\n-----END CERTIFICATE-----\n\n"
# Append intermediary certificates.
pem += urllib.request.urlopen("https://www.gandi.net/static/CAs/GandiStandardSSLCA.pem").read().decode("ascii")
# Write out.
with open(ssl_certificate, "w") as f:
f.write(pem)
print("The certificate has been installed in %s. Restarting services..." % ssl_certificate)
# Restart dovecot and if this is for PRIMARY_HOSTNAME.
if domain == env['PRIMARY_HOSTNAME']:
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
# Restart nginx in all cases.
shell('check_call', ["/usr/sbin/service", "nginx", "restart"])
else:
print("The certificate has an unknown status. Please check https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
if __name__ == "__main__":
if len(sys.argv) < 4:
print("Usage: python management/buy_certificate.py gandi_api_key domain_name {purchase, setup}")
sys.exit(1)
api_key = sys.argv[1]
domain_name = sys.argv[2]
cmd = sys.argv[3]
buy_ssl_certificate(api_key, domain_name, cmd, load_environment())

View File

@@ -11,6 +11,12 @@ from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_u
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
# Create a worker pool for the status checks. The pool should
# live across http requests so we don't baloon the system with
# processes.
import multiprocessing.pool
pool = multiprocessing.pool.Pool(processes=10)
env = utils.load_environment() env = utils.load_environment()
auth_service = auth.KeyAuthService() auth_service = auth.KeyAuthService()
@@ -272,7 +278,7 @@ def dns_get_dump():
@authorized_personnel_only @authorized_personnel_only
def ssl_get_csr(domain): def ssl_get_csr(domain):
from web_update import get_domain_ssl_files, create_csr from web_update import get_domain_ssl_files, create_csr
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
return create_csr(domain, ssl_key, env) return create_csr(domain, ssl_key, env)
@app.route('/ssl/install', methods=['POST']) @app.route('/ssl/install', methods=['POST'])
@@ -318,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) run_checks(env, output, pool)
return json_response(output.items) return json_response(output.items)
@app.route('/system/updates') @app.route('/system/updates')

View File

@@ -122,7 +122,7 @@ def do_dns_update(env, force=False):
shell('check_call', ["/usr/sbin/service", "nsd", "restart"]) shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
# Write the OpenDKIM configuration tables. # Write the OpenDKIM configuration tables.
if write_opendkim_tables(zonefiles, env): if write_opendkim_tables(domains, env):
# Settings changed. Kick opendkim. # Settings changed. Kick opendkim.
shell('check_call', ["/usr/sbin/service", "opendkim", "restart"]) shell('check_call', ["/usr/sbin/service", "opendkim", "restart"])
if len(updated_domains) == 0: if len(updated_domains) == 0:
@@ -222,7 +222,11 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
for qname, rtype, value, explanation in defaults: for qname, rtype, value, explanation in defaults:
if value is None or value.strip() == "": continue # skip IPV6 if not set if value is None or value.strip() == "": continue # skip IPV6 if not set
if not is_zone and qname == "www": continue # don't create any default 'www' subdomains on what are themselves subdomains if not is_zone and qname == "www": continue # don't create any default 'www' subdomains on what are themselves subdomains
if not has_rec(qname, rtype) and not has_rec(qname, "CNAME"): # Set the default record, but not if:
# (1) there is not a user-set record of the same type already
# (2) there is not a CNAME record already, since you can't set both and who knows what takes precedence
# (2) there is not an A record already (if this is an A record this is a dup of (1), and if this is an AAAA record then don't set a default AAAA record if the user sets a custom A record, since the default wouldn't make sense and it should not resolve if the user doesn't provide a new AAAA record)
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))
# Append the DKIM TXT record to the zone as generated by OpenDKIM. # Append the DKIM TXT record to the zone as generated by OpenDKIM.
@@ -254,6 +258,10 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
def get_custom_records(domain, additional_records, env): def get_custom_records(domain, additional_records, env):
for qname, value in additional_records.items(): for qname, value in additional_records.items():
# We don't count the secondary nameserver config (if present) as a record - that would just be
# confusing to users. Instead it is accessed/manipulated directly via (get/set)_custom_dns_config.
if qname == "_secondary_nameserver": continue
# Is this record for the domain or one of its subdomains? # Is this record for the domain or one of its subdomains?
# If `domain` is None, return records for all domains. # If `domain` is None, return records for all domains.
if domain is not None and qname != domain and not qname.endswith("." + domain): continue if domain is not None and qname != domain and not qname.endswith("." + domain): continue
@@ -612,8 +620,9 @@ def sign_zone(domain, zonefile, env):
######################################################################## ########################################################################
def write_opendkim_tables(zonefiles, env): def write_opendkim_tables(domains, env):
# Append a record to OpenDKIM's KeyTable and SigningTable for each domain. # Append a record to OpenDKIM's KeyTable and SigningTable for each domain
# that we send mail from (zones and all subdomains).
opendkim_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private') opendkim_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private')
@@ -632,7 +641,7 @@ def write_opendkim_tables(zonefiles, env):
"SigningTable": "SigningTable":
"".join( "".join(
"*@{domain} {domain}\n".format(domain=domain) "*@{domain} {domain}\n".format(domain=domain)
for domain, zonefile in zonefiles for domain in domains
), ),
# The KeyTable specifies the signing domain, the DKIM selector, and the # The KeyTable specifies the signing domain, the DKIM selector, and the
@@ -641,7 +650,7 @@ def write_opendkim_tables(zonefiles, env):
"KeyTable": "KeyTable":
"".join( "".join(
"{domain} {domain}:mail:{key_file}\n".format(domain=domain, key_file=opendkim_key_file) "{domain} {domain}:mail:{key_file}\n".format(domain=domain, key_file=opendkim_key_file)
for domain, zonefile in zonefiles for domain in domains
), ),
} }

View File

@@ -17,12 +17,12 @@ 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): def run_checks(env, output, pool):
# run systems checks # run systems checks
output.add_heading("System") output.add_heading("System")
# check that services are running # check that services are running
if not run_services_checks(env, output): if not run_services_checks(env, output, pool):
# If critical services are not running, stop. If bind9 isn't running, # If critical services are not running, stop. If bind9 isn't running,
# all later DNS checks will timeout and that will take forever to # all later DNS checks will timeout and that will take forever to
# go through, and if running over the web will cause a fastcgi timeout. # go through, and if running over the web will cause a fastcgi timeout.
@@ -37,13 +37,21 @@ def run_checks(env, output):
# perform other checks asynchronously # perform other checks asynchronously
pool = multiprocessing.pool.Pool(processes=1) run_network_checks(env, output)
r1 = pool.apply_async(run_network_checks, [env]) run_domain_checks(env, output, pool)
r2 = run_domain_checks(env)
r1.get().playback(output)
r2.playback(output)
def run_services_checks(env, output): def get_ssh_port():
# Returns ssh port
output = shell('check_output', ['sshd', '-T'])
returnNext = False
for e in output.split():
if returnNext:
return int(e)
if e == "port":
returnNext = True
def run_services_checks(env, output, pool):
# Check that system services are running. # Check that system services are running.
services = [ services = [
@@ -54,11 +62,12 @@ def run_services_checks(env, output):
{ "name": "Postgrey", "port": 10023, "public": False, }, { "name": "Postgrey", "port": 10023, "public": False, },
{ "name": "Spamassassin", "port": 10025, "public": False, }, { "name": "Spamassassin", "port": 10025, "public": False, },
{ "name": "OpenDKIM", "port": 8891, "public": False, }, { "name": "OpenDKIM", "port": 8891, "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": True, },
{ "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": 22, "public": True, }, { "name": "SSH Login (ssh)", "port": get_ssh_port(), "public": True, },
{ "name": "Public DNS (nsd4)", "port": 53, "public": True, }, { "name": "Public DNS (nsd4)", "port": 53, "public": True, },
{ "name": "Incoming Mail (SMTP/postfix)", "port": 25, "public": True, }, { "name": "Incoming Mail (SMTP/postfix)", "port": 25, "public": True, },
{ "name": "Outgoing Mail (SMTP 587/postfix)", "port": 587, "public": True, }, { "name": "Outgoing Mail (SMTP 587/postfix)", "port": 587, "public": True, },
@@ -70,7 +79,6 @@ def run_services_checks(env, output):
all_running = True all_running = True
fatal = False fatal = False
pool = multiprocessing.pool.Pool(processes=10)
ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(services)), chunksize=1) ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(services)), chunksize=1)
for i, running, fatal2, output2 in sorted(ret): for i, running, fatal2, output2 in sorted(ret):
all_running = all_running and running all_running = all_running and running
@@ -90,13 +98,28 @@ def check_service(i, service, env):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1) s.settimeout(1)
try: try:
s.connect(( try:
"127.0.0.1" if not service["public"] else env['PUBLIC_IP'], s.connect((
service["port"])) "127.0.0.1" if not service["public"] else env['PUBLIC_IP'],
running = True service["port"]))
running = True
except OSError as e1:
if service["public"] and service["port"] != 53:
# For public services (except DNS), try the private IP as a fallback.
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.settimeout(1)
try:
s1.connect(("127.0.0.1", service["port"]))
output.print_error("%s is running but is not publicly accessible at %s:%d (%s)." % (service['name'], env['PUBLIC_IP'], service['port'], str(e1)))
except:
raise e1
finally:
s1.close()
else:
raise
except OSError as e: except OSError as e:
output.print_error("%s is not running (%s)." % (service['name'], str(e))) output.print_error("%s is not running (%s; port %d)." % (service['name'], str(e), service['port']))
# Why is nginx not running? # Why is nginx not running?
if service["port"] in (80, 443): if service["port"] in (80, 443):
@@ -162,10 +185,9 @@ def check_free_disk_space(env, output):
else: else:
output.print_error(disk_msg) output.print_error(disk_msg)
def run_network_checks(env): def run_network_checks(env, output):
# Also see setup/network-checks.sh. # Also see setup/network-checks.sh.
output = BufferedOutput()
output.add_heading("Network") output.add_heading("Network")
# Stop if we cannot make an outbound connection on port 25. Many residential # Stop if we cannot make an outbound connection on port 25. Many residential
@@ -193,9 +215,7 @@ def run_network_checks(env):
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']))
return output def run_domain_checks(env, output, pool):
def run_domain_checks(env):
# 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)
@@ -215,13 +235,10 @@ def run_domain_checks(env):
# 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, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
for domain in domains_to_check) for domain in domains_to_check)
pool = multiprocessing.pool.Pool(processes=10)
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 }
output = BufferedOutput()
for domain in sort_domains(ret, env): for domain in sort_domains(ret, env):
ret[domain].playback(output) ret[domain].playback(output)
return output
def run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains): def run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains):
output = BufferedOutput() output = BufferedOutput()
@@ -399,13 +416,17 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
def check_mail_domain(domain, env, output): def check_mail_domain(domain, env, output):
# Check the MX record. # Check the MX record.
recommended_mx = "10 " + env['PRIMARY_HOSTNAME']
mx = query_dns(domain, "MX", nxdomain=None) mx = query_dns(domain, "MX", nxdomain=None)
expected_mx = "10 " + env['PRIMARY_HOSTNAME']
if mx == expected_mx: if mx is None:
output.print_ok("Domain's email is directed to this domain. [%s => %s]" % (domain, mx)) mxhost = None
else:
# query_dns returns a semicolon-delimited list
# of priority-host pairs.
mxhost = mx.split('; ')[0].split(' ')[1]
elif mx == None: if mxhost == None:
# A missing MX record is okay on the primary hostname because # A missing MX record is okay on the primary hostname because
# the primary hostname's A record (the MX fallback) is... itself, # the primary hostname's A record (the MX fallback) is... itself,
# which is what we want the MX to be. # which is what we want the MX to be.
@@ -423,15 +444,22 @@ def check_mail_domain(domain, env, output):
else: else:
output.print_error("""This domain's DNS MX record is not set. It should be '%s'. Mail will not output.print_error("""This domain's DNS MX record is not set. It should be '%s'. Mail will not
be delivered to this box. It may take several hours for public DNS to update after a be delivered to this box. It may take several hours for public DNS to update after a
change. This problem may result from other issues listed here.""" % (expected_mx,)) change. This problem may result from other issues listed here.""" % (recommended_mx,))
elif mxhost == env['PRIMARY_HOSTNAME']:
good_news = "Domain's email is directed to this domain. [%s => %s]" % (domain, mx)
if mx != recommended_mx:
good_news += " This configuration is non-standard. The recommended configuration is '%s'." % (recommended_mx,)
output.print_ok(good_news)
else: else:
output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '%s' but should be '%s'. Mail will not output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '%s' but should be '%s'. Mail will not
be delivered to this box. It may take several hours for public DNS to update after a change. This problem may result from be delivered to this box. It may take several hours for public DNS to update after a change. This problem may result from
other issues listed here.""" % (mx, expected_mx)) other issues listed here.""" % (mx, recommended_mx))
# Check that the postmaster@ email address exists. # Check that the postmaster@ email address exists. Not required if the domain has a
check_alias_exists("postmaster@" + domain, env, output) # catch-all address or domain alias.
if "@" + domain not in dict(get_mail_aliases(env)):
check_alias_exists("postmaster@" + domain, env, output)
# Stop if the domain is listed in the Spamhaus Domain Block List. # Stop if the domain is listed in the Spamhaus Domain Block List.
# The user might have chosen a domain that was previously in use by a spammer # The user might have chosen a domain that was previously in use by a spammer
@@ -494,7 +522,7 @@ def check_ssl_cert(domain, env, output):
if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return
# Where is the SSL stored? # Where is the SSL stored?
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
if not os.path.exists(ssl_certificate): if not os.path.exists(ssl_certificate):
output.print_error("The SSL certificate file for this domain is missing.") output.print_error("The SSL certificate file for this domain is missing.")
@@ -506,7 +534,7 @@ def check_ssl_cert(domain, env, output):
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.
output.print_ok("SSL certificate is signed & valid. " + cert_status_details) output.print_ok("SSL certificate is signed & valid. %s %s" % (ssl_via if ssl_via else "", cert_status_details))
elif cert_status == "SELF-SIGNED": elif cert_status == "SELF-SIGNED":
# Offer instructions for purchasing a signed certificate. # Offer instructions for purchasing a signed certificate.
@@ -748,21 +776,25 @@ class BufferedOutput:
for attr, args, kwargs in self.buf: for attr, args, kwargs in self.buf:
getattr(output, attr)(*args, **kwargs) getattr(output, attr)(*args, **kwargs)
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
from utils import load_environment from utils import load_environment
env = load_environment() env = load_environment()
if len(sys.argv) == 1: if len(sys.argv) == 1:
run_checks(env, ConsoleOutput()) pool = multiprocessing.pool.Pool(processes=10)
run_checks(env, ConsoleOutput(), pool)
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']
if query_dns(domain, "A") != env['PUBLIC_IP']: if query_dns(domain, "A") != env['PUBLIC_IP']:
sys.exit(1) sys.exit(1)
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
if not os.path.exists(ssl_certificate): if not os.path.exists(ssl_certificate):
sys.exit(1) sys.exit(1)
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key) cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
if cert_status != "OK": if cert_status != "OK":
sys.exit(1) sys.exit(1)
sys.exit(0) sys.exit(0)

View File

@@ -40,7 +40,7 @@
<h4>Exchange/ActiveSync settings</h4> <h4>Exchange/ActiveSync settings</h4>
<p>On iOS devices and devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, you may set up your mail as an Exchange or ActiveSync server. However, we&rsquo;ve found this to be more buggy than using IMAP. If you encounter any problems, please use the manual settings above.</p> <p>On iOS devices, devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, or using Outlook 2007 or later on Windows 7 and later, you may set up your mail as an Exchange or ActiveSync server. However, we&rsquo;ve found this to be more buggy than using IMAP as described above. If you encounter any problems, please use the manual settings above.</p>
<table class="table"> <table class="table">
<tr><th>Server</th> <td>{{hostname}}</td></tr> <tr><th>Server</th> <td>{{hostname}}</td></tr>

View File

@@ -7,7 +7,7 @@
<h3>Uploading web files</h3> <h3>Uploading web files</h3>
<p>You can replace the default website with your own HTML pages and other static files. This control panel won&rsquo;t help you design a website, but once you have <tt>.html</tt> files you can upload it following these instructions:</p> <p>You can replace the default website with your own HTML pages and other static files. This control panel won&rsquo;t help you design a website, but once you have <tt>.html</tt> files you can upload them following these instructions:</p>
<ol> <ol>
<li>Ensure that any domains you are publishing a website for have no problems on the <a href="#system_status" onclick="return show_panel(this);">Status Checks</a> page.</li> <li>Ensure that any domains you are publishing a website for have no problems on the <a href="#system_status" onclick="return show_panel(this);">Status Checks</a> page.</li>
@@ -18,41 +18,24 @@
<li>Upload your <tt>.html</tt> or other files to the directory <tt>{{storage_root}}/www/default</tt> on this machine. They will appear directly and immediately on the web.</li> <li>Upload your <tt>.html</tt> or other files to the directory <tt>{{storage_root}}/www/default</tt> on this machine. They will appear directly and immediately on the web.</li>
<li>The websites set up on this machine are listed in the table below with where to put the files for each website (if you have customized that, see next section).</li> <li>The websites set up on this machine are listed in the table below with where to put the files for each website.</li>
<table id="web_domains_existing" class="table" style="margin-bottom: 2em; width: auto;"> <table id="web_domains_existing" class="table" style="margin-bottom: 1em; width: auto;">
<thead> <thead>
<tr> <tr>
<th>Site</th> <th>Site</th>
<th>Directory for Files</th> <th>Directory for Files</th>
<th/>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
<li>If you want to have this box host a static website on a domain that is not listed in the table, create a dummy <a href="#users" onclick="return show_panel(this);">mail user</a> or <a href="#aliases" onclick="return show_panel(this);">alias</a> on the domain first.</li> <p>To add a domain to this table, create a dummy <a href="#users" onclick="return show_panel(this);">mail user</a> or <a href="#aliases" onclick="return show_panel(this);">alias</a> on the domain first and see the <a href="https://mailinabox.email/guide.html#domain-name-configuration">setup guide</a> for adding nameserver records to the new domain at your registrar (but <i>not</i> glue records).</p>
</ol> </ol>
<h3>Different sites for different domains</h3>
<p>Create one of the directories shown in the table below to create a space for different files for one of the websites.</p>
<p>After you create one of these directories, click <button id="web_update" class="btn btn-primary" onclick="do_web_update()">Web Update</button> to restart the box&rsquo;s web server so that it sees the new website file location.</p>
<table id="web_domains_custom" class="table" style="margin-bottom: 2em; width: auto;">
<thead>
<tr>
<th>Site</th>
<th>Create Directory</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script> <script>
function show_web() { function show_web() {
api( api(
@@ -64,24 +47,18 @@ function show_web() {
var tb = $('#web_domains_existing tbody'); var tb = $('#web_domains_existing tbody');
tb.text(''); tb.text('');
for (var i = 0; i < domains.length; i++) { for (var i = 0; i < domains.length; i++) {
var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt/></td></tr>"); if (!domains[i].static_enabled) continue;
var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt/></td> <td class='change-root hidden'><button class='btn btn-default btn-xs' onclick='show_change_web_root(this)'>Change</button></td></tr>");
tb.append(row); tb.append(row);
row.attr('data-domain', domains[i].domain);
row.attr('data-custom-web-root', domains[i].custom_root);
row.find('.domain a').text('https://' + domains[i].domain); row.find('.domain a').text('https://' + domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain); row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.find('.directory tt').text(domains[i].root); row.find('.directory tt').text(domains[i].root);
if (domains[i].root != domains[i].custom_root)
row.find('.change-root').removeClass('hidden');
} }
tb = $('#web_domains_custom tbody');
tb.text('');
for (var i = 0; i < domains.length; i++) {
if (domains[i].root != domains[i].custom_root) {
var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt></td></tr>");
tb.append(row);
row.find('.domain a').text('https://' + domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.find('.directory tt').text(domains[i].custom_root);
}
}
}); });
} }
@@ -99,4 +76,14 @@ function do_web_update() {
show_modal_error("Web Update", data, function() { show_web() }); show_modal_error("Web Update", data, function() { show_web() });
}); });
} }
function show_change_web_root(elem) {
var domain = $(elem).parents('tr').attr('data-domain');
var root = $(elem).parents('tr').attr('data-custom-web-root');
show_modal_confirm(
'Change Root Directory for ' + domain,
'<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.',
'Update',
function() { do_web_update(); });
}
</script> </script>

View File

@@ -17,9 +17,8 @@ def get_web_domains(env):
domains.add(env['PRIMARY_HOSTNAME']) domains.add(env['PRIMARY_HOSTNAME'])
# Also serve web for all mail domains so that we might at least # Also serve web for all mail domains so that we might at least
# provide Webfinger and ActiveSync auto-discover of email settings # provide auto-discover of email settings, and also a static website
# (though the latter isn't really working). These will require that # if the user wants to make one. These will require an SSL cert.
# an SSL cert be installed.
domains |= get_mail_domains(env) domains |= get_mail_domains(env)
# ...Unless the domain has an A/AAAA record that maps it to a different # ...Unless the domain has an A/AAAA record that maps it to a different
@@ -28,6 +27,7 @@ def get_web_domains(env):
for domain, value in dns.items(): for domain, value in dns.items():
if domain not in domains: continue if domain not in domains: continue
if (isinstance(value, str) and (value != "local")) \ if (isinstance(value, str) and (value != "local")) \
or (isinstance(value, dict) and ("CNAME" in value)) \
or (isinstance(value, dict) and ("A" in value) and (value["A"] != "local")) \ or (isinstance(value, dict) and ("A" in value) and (value["A"] != "local")) \
or (isinstance(value, dict) and ("AAAA" in value) and (value["AAAA"] != "local")): or (isinstance(value, dict) and ("AAAA" in value) and (value["AAAA"] != "local")):
domains.remove(domain) domains.remove(domain)
@@ -75,7 +75,7 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
root = get_web_root(domain, env) root = get_web_root(domain, env)
# What private key and SSL certificate will we use for this domain? # What private key and SSL certificate will we use for this domain?
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
# For hostnames created after the initial setup, ensure we have an SSL certificate # For hostnames created after the initial setup, ensure we have an SSL certificate
# available. Make a self-signed one now if one doesn't exist. # available. Make a self-signed one now if one doesn't exist.
@@ -149,6 +149,7 @@ def get_domain_ssl_files(domain, env, allow_shared_cert=True):
# What SSL certificate will we use? # What SSL certificate will we use?
ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem')
ssl_via = None
if domain == env['PRIMARY_HOSTNAME']: if domain == env['PRIMARY_HOSTNAME']:
# For PRIMARY_HOSTNAME, use the one we generated at set-up time. # For PRIMARY_HOSTNAME, use the one we generated at set-up time.
ssl_certificate = ssl_certificate_primary ssl_certificate = ssl_certificate_primary
@@ -163,8 +164,16 @@ def get_domain_ssl_files(domain, env, allow_shared_cert=True):
from status_checks import check_certificate from status_checks import check_certificate
if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK": if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK":
ssl_certificate = ssl_certificate_primary ssl_certificate = ssl_certificate_primary
ssl_via = "Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']
return ssl_key, ssl_certificate # For a 'www.' domain, see if we can reuse the cert of the parent.
elif domain.startswith('www.'):
ssl_certificate_parent = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain[4:]))
if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None)[0] == "OK":
ssl_certificate = ssl_certificate_parent
ssl_via = "Using multi/wildcard certificate of %s." % domain[4:]
return ssl_key, ssl_certificate, ssl_via
def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env): def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env):
# For domains besides PRIMARY_HOSTNAME, generate a self-signed certificate if # For domains besides PRIMARY_HOSTNAME, generate a self-signed certificate if
@@ -219,7 +228,7 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
# Do validation on the certificate before installing it. # Do validation on the certificate before installing it.
from status_checks import check_certificate from status_checks import check_certificate
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env, allow_shared_cert=False) ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env, allow_shared_cert=False)
cert_status, cert_status_details = check_certificate(domain, fn, ssl_key) cert_status, cert_status_details = check_certificate(domain, fn, ssl_key)
if cert_status != "OK": if cert_status != "OK":
if cert_status == "SELF-SIGNED": if cert_status == "SELF-SIGNED":
@@ -250,18 +259,28 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
return "\n".join(r for r in ret if r.strip() != "") return "\n".join(r for r in ret if r.strip() != "")
def get_web_domains_info(env): def get_web_domains_info(env):
# load custom settings so we can tell what domains have a redirect or proxy set up on '/',
# which means static hosting is not happening
custom_settings = { }
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
def has_root_proxy_or_redirect(domain):
return custom_settings.get(domain, {}).get('redirects', {}).get('/') or custom_settings.get(domain, {}).get('proxies', {}).get('/')
# for the SSL config panel, get cert status
def check_cert(domain): def check_cert(domain):
from status_checks import check_certificate from status_checks import check_certificate
ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
if not os.path.exists(ssl_certificate): if not os.path.exists(ssl_certificate):
return ("danger", "No Certificate Installed") return ("danger", "No Certificate Installed")
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key) cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
if cert_status == "OK": if cert_status == "OK":
if domain == env['PRIMARY_HOSTNAME'] or ssl_certificate != get_domain_ssl_files(env['PRIMARY_HOSTNAME'], env)[1]: if not ssl_via:
return ("success", "Signed & valid. " + cert_status_details) return ("success", "Signed & valid. " + cert_status_details)
else: else:
# This is an alternate domain but using the same cert as the primary domain. # This is an alternate domain but using the same cert as the primary domain.
return ("success", "Signed & valid. Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']) return ("success", "Signed & valid. " + ssl_via)
elif cert_status == "SELF-SIGNED": elif cert_status == "SELF-SIGNED":
return ("warning", "Self-signed. Get a signed certificate to stop warnings.") return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
else: else:
@@ -273,6 +292,7 @@ def get_web_domains_info(env):
"root": get_web_root(domain, env), "root": get_web_root(domain, env),
"custom_root": get_web_root(domain, env, test_exists=False), "custom_root": get_web_root(domain, env, test_exists=False),
"ssl_certificate": check_cert(domain), "ssl_certificate": check_cert(domain),
"static_enabled": not has_root_proxy_or_redirect(domain),
} }
for domain in get_web_domains(env) for domain in get_web_domains(env)
] ]

View File

@@ -7,7 +7,7 @@
######################################################### #########################################################
if [ -z "$TAG" ]; then if [ -z "$TAG" ]; then
TAG=v0.06 TAG=v0.07
fi fi
# Are we running as root? # Are we running as root?

20
setup/dkim.sh Normal file → Executable file
View File

@@ -10,7 +10,7 @@ source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars source /etc/mailinabox.conf # load global vars
# Install DKIM... # Install DKIM...
apt_install opendkim opendkim-tools apt_install opendkim opendkim-tools opendmarc
# Make sure configuration directories exist. # Make sure configuration directories exist.
mkdir -p /etc/opendkim; mkdir -p /etc/opendkim;
@@ -48,15 +48,25 @@ fi
chown -R opendkim:opendkim $STORAGE_ROOT/mail/dkim chown -R opendkim:opendkim $STORAGE_ROOT/mail/dkim
chmod go-rwx $STORAGE_ROOT/mail/dkim chmod go-rwx $STORAGE_ROOT/mail/dkim
# Add OpenDKIM as a milter to postfix, which is how it intercepts outgoing tools/editconf.py /etc/opendmarc.conf -s \
# mail to perform the signing (by adding a mail header). "Syslog=true" \
# Be careful. If we add other milters later, it needs to be concatenated on the smtpd_milters line. #NODOC "Socket=inet:8893@[127.0.0.1]"
# Add OpenDKIM and OpenDMARC as milters to postfix, which is how OpenDKIM
# intercepts outgoing mail to perform the signing (by adding a mail header)
# and how they both intercept incoming mail to add Authentication-Results
# headers. The order possibly/probably matters: OpenDMARC relies on the
# OpenDKIM Authentication-Results header already being present.
#
# Be careful. If we add other milters later, this needs to be concatenated
# on the smtpd_milters line.
tools/editconf.py /etc/postfix/main.cf \ tools/editconf.py /etc/postfix/main.cf \
smtpd_milters=inet:127.0.0.1:8891 \ "smtpd_milters=inet:127.0.0.1:8891 inet:127.0.0.1:8893"\
non_smtpd_milters=\$smtpd_milters \ non_smtpd_milters=\$smtpd_milters \
milter_default_action=accept milter_default_action=accept
# Restart services. # Restart services.
restart_service opendkim restart_service opendkim
restart_service opendmarc
restart_service postfix restart_service postfix

View File

@@ -22,6 +22,20 @@ function hide_output {
rm -f $OUTPUT rm -f $OUTPUT
} }
function apt_get_quiet {
# Run apt-get in a totally non-interactive mode.
#
# Somehow all of these options are needed to get it to not ask the user
# questions about a) whether to proceed (-y), b) package options (noninteractive),
# and c) what to do about files changed locally (we don't cause that to happen but
# some VM providers muck with their images; -o).
#
# Although we could pass -qq to apt-get to make output quieter, many packages write to stdout
# and stderr things that aren't really important. Use our hide_output function to capture
# all of that and only show it if there is a problem (i.e. if apt_get returns a failure exit status).
DEBIAN_FRONTEND=noninteractive hide_output apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" "$@"
}
function apt_install { function apt_install {
# Report any packages already installed. # Report any packages already installed.
PACKAGES=$@ PACKAGES=$@
@@ -46,18 +60,10 @@ function apt_install {
echo installing $TO_INSTALL... echo installing $TO_INSTALL...
fi fi
# 'DEBIAN_FRONTEND=noninteractive' is to prevent dbconfig-common from asking you questions. # We still include the whole original package list in the apt-get command in
#
# Although we could pass -qq to apt-get to make output quieter, many packages write to stdout
# and stderr things that aren't really important. Use our hide_output function to capture
# all of that and only show it if there is a problem (i.e. if apt_get returns a failure exit status).
#
# Also note that we still include the whole original package list in the apt-get command in
# case it wants to upgrade anything, I guess? Maybe we can remove it. Doesn't normally make # case it wants to upgrade anything, I guess? Maybe we can remove it. Doesn't normally make
# a difference. # a difference.
DEBIAN_FRONTEND=noninteractive \ apt_get_quiet install $PACKAGES
hide_output \
apt-get -y install $PACKAGES
} }
function get_default_hostname { function get_default_hostname {

View File

@@ -30,5 +30,8 @@ $(pwd)/management/backup.py
EOF EOF
chmod +x /etc/cron.daily/mailinabox-backup chmod +x /etc/cron.daily/mailinabox-backup
# Start it. # Start it. Remove the api key file first so that start.sh
# can wait for it to be created to know that the management
# server is ready.
rm -f /var/lib/mailinabox/api.key
restart_service mailinabox restart_service mailinabox

View File

@@ -1,6 +1,6 @@
# Install the 'host', 'sed', and and 'nc' tools. This script is run before # Install the 'host', 'sed', and and 'nc' tools. This script is run before
# the rest of the system setup so we may not yet have things installed. # the rest of the system setup so we may not yet have things installed.
hide_output apt-get -y install bind9-host sed netcat-openbsd apt_get_quiet install bind9-host sed netcat-openbsd
# Stop if the PRIMARY_HOSTNAME is listed in the Spamhaus Domain Block List. # Stop if the PRIMARY_HOSTNAME is listed in the Spamhaus Domain Block List.
# The user might have chosen a name that was previously in use by a spammer # The user might have chosen a name that was previously in use by a spammer

View File

@@ -4,7 +4,7 @@ if [ -z "$NONINTERACTIVE" ]; then
# e.g. if we piped a bootstrapping install script to bash to get started. In that # e.g. if we piped a bootstrapping install script to bash to get started. In that
# case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we # case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we
# use a shell flag instead. Really supress any output from installing dialog. # use a shell flag instead. Really supress any output from installing dialog.
hide_output apt-get -y install dialog apt_get_quiet install dialog
message_box "Mail-in-a-Box Installation" \ message_box "Mail-in-a-Box Installation" \
"Hello and thanks for deploying a 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\nI'm going to ask you a few questions.

View File

@@ -87,17 +87,33 @@ if [ -z "$SKIP_NETWORK_CHECKS" ]; then
. setup/network-checks.sh . setup/network-checks.sh
fi fi
# For the first time (if the config file (/etc/mailinabox.conf) not exists):
# Create the user named "user-data" and store all persistent user # Create the user named "user-data" and store all persistent user
# data (mailboxes, etc.) in that user's home directory. # data (mailboxes, etc.) in that user's home directory.
#
# If the config file exists:
# Apply the existing configuration options for STORAGE_USER/ROOT
if [ -z "$STORAGE_USER" ]; then
STORAGE_USER=$([[ -z "$DEFAULT_STORAGE_USER" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
fi
if [ -z "$STORAGE_ROOT" ]; then if [ -z "$STORAGE_ROOT" ]; then
STORAGE_USER=user-data STORAGE_ROOT=$([[ -z "$DEFAULT_STORAGE_ROOT" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
if [ ! -d /home/$STORAGE_USER ]; then useradd -m $STORAGE_USER; fi fi
STORAGE_ROOT=/home/$STORAGE_USER
# Create the STORAGE_USER if it not exists
if ! id -u $STORAGE_USER >/dev/null 2>&1; then
useradd -m $STORAGE_USER
fi
# Create the STORAGE_ROOT if it not exists
if [ ! -d $STORAGE_ROOT ]; then
mkdir -p $STORAGE_ROOT mkdir -p $STORAGE_ROOT
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
# Save the global options in /etc/mailinabox.conf so that standalone # Save the global options in /etc/mailinabox.conf so that standalone
# tools know where to look for data. # tools know where to look for data.
cat > /etc/mailinabox.conf << EOF; cat > /etc/mailinabox.conf << EOF;
@@ -126,10 +142,13 @@ source setup/owncloud.sh
source setup/zpush.sh source setup/zpush.sh
source setup/management.sh source setup/management.sh
# Write the DNS and nginx configuration files. # Ping the management daemon to write the DNS and nginx configuration files.
sleep 5 # wait for the daemon to start while [ ! -f /var/lib/mailinabox/api.key ]; do
curl -s -d POSTDATA --user $(</var/lib/mailinabox/api.key): http://127.0.0.1:10222/dns/update echo Waiting for the Mail-in-a-Box management daemon to start...
curl -s -d POSTDATA --user $(</var/lib/mailinabox/api.key): http://127.0.0.1:10222/web/update sleep 2
done
tools/dns_update
tools/web_update
# If there aren't any mail users yet, create one. # If there aren't any mail users yet, create one.
source setup/firstuser.sh source setup/firstuser.sh

View File

@@ -9,7 +9,7 @@ source setup/functions.sh # load our functions
echo Updating system packages... echo Updating system packages...
hide_output apt-get update hide_output apt-get update
hide_output apt-get -y upgrade apt_get_quiet upgrade
# Install basic utilities. # Install basic utilities.
# #

View File

@@ -53,6 +53,16 @@ cat conf/ios-profile.xml \
> /var/lib/mailinabox/mobileconfig.xml > /var/lib/mailinabox/mobileconfig.xml
chmod a+r /var/lib/mailinabox/mobileconfig.xml chmod a+r /var/lib/mailinabox/mobileconfig.xml
# Create the Mozilla Auto-configuration file which is exposed via the
# nginx configuration at /.well-known/autoconfig/mail/config-v1.1.xml.
# The format of the file is documented at:
# https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
# and https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration/FileFormat/HowTo.
cat conf/mozilla-autoconfig.xml \
| sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \
> /var/lib/mailinabox/mozilla-autoconfig.xml
chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml
# make a default homepage # make a default homepage
if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration #NODOC if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration #NODOC
mkdir -p $STORAGE_ROOT/www/default mkdir -p $STORAGE_ROOT/www/default

View File

@@ -30,7 +30,7 @@ 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.
VERSION=1.0.3 VERSION=1.1.0
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
@@ -40,7 +40,7 @@ elif [[ $VERSION != `cat /usr/local/lib/roundcubemail/version` ]]; then
needs_update=1 #NODOC needs_update=1 #NODOC
fi fi
if [ $needs_update == 1 ]; then if [ $needs_update == 1 ]; then
echo installing roudcube webmail $VERSION... 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