mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-26 02:57:04 +00:00
Merge branch 'master' into usedialog
This commit is contained in:
commit
d1c7617cdb
13
README.md
13
README.md
@ -34,17 +34,16 @@ Congratulations! You should now have a working setup. You'll be given the addres
|
|||||||
The Goals
|
The Goals
|
||||||
---------
|
---------
|
||||||
|
|
||||||
* Create a push-button "Email Appliance" for everyday users.
|
I am trying to:
|
||||||
* Promote decentralization, innovation, and privacy on the web.
|
|
||||||
|
* Make deploying a good mail server easy.
|
||||||
|
* Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web.
|
||||||
* Have automated, auditable, and [idempotent](http://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
|
* Have automated, auditable, and [idempotent](http://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
|
||||||
|
|
||||||
For more background, see [The Rationale](https://github.com/mail-in-a-box/mailinabox/wiki).
|
|
||||||
|
|
||||||
What I am not trying to do:
|
|
||||||
|
|
||||||
* **Not** to be a mail server that the NSA cannot hack.
|
* **Not** to be a mail server that the NSA cannot hack.
|
||||||
* **Not** to be customizable by power users.
|
* **Not** to be customizable by power users.
|
||||||
|
|
||||||
|
For more background, see [The Rationale](https://github.com/mail-in-a-box/mailinabox/wiki).
|
||||||
|
|
||||||
The Acknowledgements
|
The Acknowledgements
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ server {
|
|||||||
rewrite ^/admin$ /admin/;
|
rewrite ^/admin$ /admin/;
|
||||||
location /admin/ {
|
location /admin/ {
|
||||||
proxy_pass http://localhost:10222/;
|
proxy_pass http://localhost:10222/;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Roundcube Webmail configuration.
|
# Roundcube Webmail configuration.
|
||||||
|
@ -172,6 +172,30 @@ def dns_update():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return (str(e), 500)
|
return (str(e), 500)
|
||||||
|
|
||||||
|
@app.route('/dns/set/<qname>', methods=['POST'])
|
||||||
|
@app.route('/dns/set/<qname>/<rtype>', methods=['POST'])
|
||||||
|
@app.route('/dns/set/<qname>/<rtype>/<value>', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def dns_set_record(qname, rtype="A", value=None):
|
||||||
|
from dns_update import do_dns_update, set_custom_dns_record
|
||||||
|
try:
|
||||||
|
# Get the value from the URL, then the POST parameters, or if it is not set then
|
||||||
|
# use the remote IP address of the request --- makes dynamic DNS easy. To clear a
|
||||||
|
# value, '' must be explicitly passed.
|
||||||
|
print(request.environ)
|
||||||
|
if value is None:
|
||||||
|
value = request.form.get("value")
|
||||||
|
if value is None:
|
||||||
|
value = request.environ.get("HTTP_X_FORWARDED_FOR") # normally REMOTE_ADDR but we're behind nginx as a reverse proxy
|
||||||
|
if value == '':
|
||||||
|
# request deletion
|
||||||
|
value = None
|
||||||
|
if set_custom_dns_record(qname, rtype, value, env):
|
||||||
|
return do_dns_update(env)
|
||||||
|
return "OK"
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
@app.route('/dns/dump')
|
@app.route('/dns/dump')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def dns_get_dump():
|
def dns_get_dump():
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
import os, os.path, urllib.parse, datetime, re, hashlib
|
import os, os.path, urllib.parse, datetime, re, hashlib
|
||||||
|
import ipaddress
|
||||||
import rtyaml
|
import rtyaml
|
||||||
|
|
||||||
from mailconfig import get_mail_domains
|
from mailconfig import get_mail_domains
|
||||||
@ -551,6 +552,79 @@ def write_opendkim_tables(zonefiles, env):
|
|||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
|
def set_custom_dns_record(qname, rtype, value, env):
|
||||||
|
# validate
|
||||||
|
rtype = rtype.upper()
|
||||||
|
if value is not None:
|
||||||
|
if rtype in ("A", "AAAA"):
|
||||||
|
v = ipaddress.ip_address(value)
|
||||||
|
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.")
|
||||||
|
elif rtype in ("CNAME", "TXT"):
|
||||||
|
# anything goes
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown record type '%s'." % rtype)
|
||||||
|
|
||||||
|
# load existing config
|
||||||
|
config = get_custom_dns_config(env)
|
||||||
|
|
||||||
|
# update
|
||||||
|
if qname not in config:
|
||||||
|
if value is None:
|
||||||
|
# Is asking to delete a record that does not exist.
|
||||||
|
return False
|
||||||
|
elif rtype == "A":
|
||||||
|
# Add this record using the short form 'qname: value'.
|
||||||
|
config[qname] = value
|
||||||
|
else:
|
||||||
|
# Add this record. This is the qname's first record.
|
||||||
|
config[qname] = { rtype: value }
|
||||||
|
else:
|
||||||
|
if isinstance(config[qname], str):
|
||||||
|
# This is a short-form 'qname: value' implicit-A record.
|
||||||
|
if value is None and rtype != "A":
|
||||||
|
# Is asking to delete a record that doesn't exist.
|
||||||
|
return False
|
||||||
|
elif value is None and rtype == "A":
|
||||||
|
# Delete record.
|
||||||
|
del config[qname]
|
||||||
|
elif rtype == "A":
|
||||||
|
# Update, keeping short form.
|
||||||
|
if config[qname] == "value":
|
||||||
|
# No change.
|
||||||
|
return False
|
||||||
|
config[qname] = value
|
||||||
|
else:
|
||||||
|
# Expand short form so we can add a new record type.
|
||||||
|
config[qname] = { "A": config[qname], rtype: value }
|
||||||
|
else:
|
||||||
|
# This is the qname: { ... } (dict) format.
|
||||||
|
if value is None:
|
||||||
|
if rtype not in config[qname]:
|
||||||
|
# Is asking to delete a record that doesn't exist.
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Delete the record. If it's the last record, delete the domain.
|
||||||
|
del config[qname][rtype]
|
||||||
|
if len(config[qname]) == 0:
|
||||||
|
del config[qname]
|
||||||
|
else:
|
||||||
|
# Update the record.
|
||||||
|
if config[qname].get(rtype) == "value":
|
||||||
|
# No change.
|
||||||
|
return False
|
||||||
|
config[qname][rtype] = value
|
||||||
|
|
||||||
|
# serialize & save
|
||||||
|
config_yaml = rtyaml.dump(config)
|
||||||
|
with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), "w") as f:
|
||||||
|
f.write(config_yaml)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
|
||||||
def justtestingdotemail(domain, records):
|
def justtestingdotemail(domain, records):
|
||||||
# If the domain is a subdomain of justtesting.email, which we own,
|
# If the domain is a subdomain of justtesting.email, which we own,
|
||||||
# automatically populate the zone where it is set up on dns4e.com.
|
# automatically populate the zone where it is set up on dns4e.com.
|
||||||
|
@ -177,6 +177,7 @@ def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, en
|
|||||||
"openssl", "req", "-new",
|
"openssl", "req", "-new",
|
||||||
"-key", ssl_key,
|
"-key", ssl_key,
|
||||||
"-out", csr_path,
|
"-out", csr_path,
|
||||||
|
"-sha256",
|
||||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
||||||
|
|
||||||
# And then make the certificate.
|
# And then make the certificate.
|
||||||
|
@ -8,7 +8,7 @@ hide_output pip3 install rtyaml
|
|||||||
# Create a backup directory and a random key for encrypting backups.
|
# Create a backup directory and a random key for encrypting backups.
|
||||||
mkdir -p $STORAGE_ROOT/backup
|
mkdir -p $STORAGE_ROOT/backup
|
||||||
if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then
|
if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then
|
||||||
openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt
|
$(umask 077; openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Link the management server daemon into a well known location.
|
# Link the management server daemon into a well known location.
|
||||||
|
@ -56,6 +56,10 @@ def migration_4(env):
|
|||||||
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
||||||
shell("check_call", ["sqlite3", db, "ALTER TABLE users ADD privileges TEXT NOT NULL DEFAULT ''"])
|
shell("check_call", ["sqlite3", db, "ALTER TABLE users ADD privileges TEXT NOT NULL DEFAULT ''"])
|
||||||
|
|
||||||
|
def migration_5(env):
|
||||||
|
# The secret key for encrypting backups was world readable. Fix here.
|
||||||
|
os.chmod(os.path.join(env["STORAGE_ROOT"], 'backup/secret_key.txt'), 0o600)
|
||||||
|
|
||||||
def get_current_migration():
|
def get_current_migration():
|
||||||
ver = 0
|
ver = 0
|
||||||
while True:
|
while True:
|
||||||
|
@ -31,7 +31,7 @@ if [ ! -f $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr ]; then
|
|||||||
# Generate a certificate signing request if one doesn't already exist.
|
# Generate a certificate signing request if one doesn't already exist.
|
||||||
hide_output \
|
hide_output \
|
||||||
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr \
|
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr \
|
||||||
-subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME"
|
-sha256 -subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME"
|
||||||
fi
|
fi
|
||||||
if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
|
||||||
# Generate a SSL certificate by self-signing if a SSL certificate doesn't yet exist.
|
# Generate a SSL certificate by self-signing if a SSL certificate doesn't yet exist.
|
||||||
|
@ -48,7 +48,7 @@ done
|
|||||||
# Remove obsoleted scripts.
|
# Remove obsoleted scripts.
|
||||||
# exchange-autodiscover is now handled by Z-Push.
|
# exchange-autodiscover is now handled by Z-Push.
|
||||||
for f in exchange-autodiscover; do
|
for f in exchange-autodiscover; do
|
||||||
rm /usr/local/bin/mailinabox-$f.php
|
rm -f /usr/local/bin/mailinabox-$f.php
|
||||||
done
|
done
|
||||||
|
|
||||||
# Make some space for users to customize their webfinger responses.
|
# Make some space for users to customize their webfinger responses.
|
||||||
|
@ -67,6 +67,7 @@ elif sys.argv[1] == "user" and len(sys.argv) == 2:
|
|||||||
# Dump a list of users, one per line. Mark admins with an asterisk.
|
# Dump a list of users, one per line. Mark admins with an asterisk.
|
||||||
users = mgmt("/mail/users?format=json", is_json=True)
|
users = mgmt("/mail/users?format=json", is_json=True)
|
||||||
for user in users:
|
for user in users:
|
||||||
|
if user['status'] == 'inactive': continue
|
||||||
print(user['email'], end='')
|
print(user['email'], end='')
|
||||||
if "admin" in user['privileges']:
|
if "admin" in user['privileges']:
|
||||||
print("*", end='')
|
print("*", end='')
|
||||||
|
Loading…
Reference in New Issue
Block a user