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

web-based administrative UI

closes #19
This commit is contained in:
Joshua Tauberer
2014-08-17 22:43:57 +00:00
parent ba8e015795
commit b30d7ad80a
19 changed files with 1527 additions and 210 deletions

View File

@@ -49,18 +49,80 @@ def open_database(env, with_connection=False):
def get_mail_users(env, as_json=False):
c = open_database(env)
c.execute('SELECT email, privileges FROM users')
# turn into a list of tuples, but sorted by domain & email address
users = { row[0]: row[1] for row in c.fetchall() } # make dict
users = [ (email, users[email]) for email in utils.sort_email_addresses(users.keys(), env) ]
if not as_json:
return [row[0] for row in c.fetchall()]
return [email for email, privileges in users]
else:
aliases = get_mail_alias_map(env)
return [
{ "email": row[0], "privileges": parse_privs(row[1]) }
for row in c.fetchall()
{
"email": email,
"privileges": parse_privs(privileges),
"status": "active",
"aliases": [
(alias, sorted(evaluate_mail_alias_map(alias, aliases, env)))
for alias in aliases.get(email.lower(), [])
]
}
for email, privileges in users
]
def get_mail_aliases(env):
def get_archived_mail_users(env):
real_users = set(get_mail_users(env))
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
ret = []
for domain_enc in os.listdir(root):
for user_enc in os.listdir(os.path.join(root, domain_enc)):
email = utils.unsafe_domain_name(user_enc) + "@" + utils.unsafe_domain_name(domain_enc)
if email in real_users: continue
ret.append({
"email": email,
"privileges": "",
"status": "inactive"
})
return ret
def get_mail_aliases(env, as_json=False):
c = open_database(env)
c.execute('SELECT source, destination FROM aliases')
return [(row[0], row[1]) for row in c.fetchall()]
aliases = { row[0]: row[1] for row in c.fetchall() } # make dict
# put in a canonical order: sort by domain, then by email address lexicographically
aliases = [ (source, aliases[source]) for source in utils.sort_email_addresses(aliases.keys(), env) ] # sort
# but put automatic aliases to administrator@ last
aliases.sort(key = lambda x : x[1] == get_system_administrator(env))
if as_json:
required_aliases = get_required_aliases(env)
aliases = [
{
"source": alias[0],
"destination": [d.strip() for d in alias[1].split(",")],
"required": alias[0] in required_aliases or alias[0] == get_system_administrator(env),
}
for alias in aliases
]
return aliases
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_mail_domains(env, filter_aliases=lambda alias : True):
def get_domain(emailaddr):
@@ -70,10 +132,30 @@ def get_mail_domains(env, filter_aliases=lambda alias : True):
+ [get_domain(source) for source, target in get_mail_aliases(env) if filter_aliases((source, target)) ]
)
def add_mail_user(email, pw, env):
def add_mail_user(email, pw, privs, env):
# validate email
if email.strip() == "":
return ("No email address provided.", 400)
if not validate_email(email, mode='user'):
return ("Invalid email address.", 400)
# validate password
if pw.strip() == "":
return ("No password provided.", 400)
if re.search(r"[\s]", pw):
return ("Passwords cannot contain spaces.", 400)
if len(pw) < 4:
return ("Passwords must be at least four characters.", 400)
# validate privileges
if privs is None or privs.strip() == "":
privs = []
else:
privs = privs.split("\n")
for p in privs:
validation = validate_privilege(p)
if validation: return validation
# get the database
conn, c = open_database(env, with_connection=True)
@@ -82,7 +164,8 @@ def add_mail_user(email, pw, env):
# add the user to the database
try:
c.execute("INSERT INTO users (email, password) VALUES (?, ?)", (email, pw))
c.execute("INSERT INTO users (email, password, privileges) VALUES (?, ?, ?)",
(email, pw, "\n".join(privs)))
except sqlite3.IntegrityError:
return ("User already exists.", 400)
@@ -142,13 +225,21 @@ def get_mail_user_privileges(email, env):
return ("That's not a user (%s)." % email, 400)
return parse_privs(rows[0][0])
def add_remove_mail_user_privilege(email, priv, action, env):
def validate_privilege(priv):
if "\n" in priv or priv.strip() == "":
return ("That's not a valid privilege (%s)." % priv, 400)
return None
def add_remove_mail_user_privilege(email, priv, action, env):
# validate
validation = validate_privilege(priv)
if validation: return validation
# get existing privs, but may fail
privs = get_mail_user_privileges(email, env)
if isinstance(privs, tuple): return privs # error
# update privs set
if action == "add":
if priv not in privs:
privs.append(priv)
@@ -157,6 +248,7 @@ def add_remove_mail_user_privilege(email, priv, action, env):
else:
return ("Invalid action.", 400)
# commit to database
conn, c = open_database(env, with_connection=True)
c.execute("UPDATE users SET privileges=? WHERE email=?", ("\n".join(privs), email))
if c.rowcount != 1:
@@ -165,20 +257,42 @@ def add_remove_mail_user_privilege(email, priv, action, env):
return "OK"
def add_mail_alias(source, destination, env, do_kick=True):
def add_mail_alias(source, destination, env, update_if_exists=False, do_kick=True):
# validate source
if source.strip() == "":
return ("No incoming email address provided.", 400)
if not validate_email(source, mode='alias'):
return ("Invalid email address.", 400)
return ("Invalid incoming email address (%s)." % source, 400)
# parse comma and \n-separated destination emails & validate
dests = []
for line in destination.split("\n"):
for email in line.split(","):
email = email.strip()
if email == "": continue
if not validate_email(email, mode='alias'):
return ("Invalid destination email address (%s)." % email, 400)
dests.append(email)
if len(destination) == 0:
return ("No destination email address(es) provided.", 400)
destination = ",".join(dests)
conn, c = open_database(env, with_connection=True)
try:
c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (source, destination))
return_status = "alias added"
except sqlite3.IntegrityError:
return ("Alias already exists (%s)." % source, 400)
if not update_if_exists:
return ("Alias already exists (%s)." % source, 400)
else:
c.execute("UPDATE aliases SET destination = ? WHERE source = ?", (destination, source))
return_status = "alias updated"
conn.commit()
if do_kick:
# Update things in case any new domains are added.
return kick(env, "alias added")
return kick(env, return_status)
def remove_mail_alias(source, env, do_kick=True):
conn, c = open_database(env, with_connection=True)
@@ -191,6 +305,35 @@ def remove_mail_alias(source, env, do_kick=True):
# Update things in case any domains are removed.
return kick(env, "alias removed")
def get_system_administrator(env):
return "administrator@" + env['PRIMARY_HOSTNAME']
def get_required_aliases(env):
# These are the aliases that must exist.
aliases = set()
# The hostmaster aliase is exposed in the DNS SOA for each zone.
aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME'])
# Get a list of domains we serve mail for, except ones for which the only
# email on that domain is a postmaster/admin alias to the administrator.
real_mail_domains = get_mail_domains(env,
filter_aliases = lambda alias : \
(not alias[0].startswith("postmaster@") \
and not alias[0].startswith("admin@")) \
or alias[1] != get_system_administrator(env) \
)
# Create postmaster@ and admin@ for all domains we serve mail on.
# postmaster@ is assumed to exist by our Postfix configuration. admin@
# isn't anything, but it might save the user some trouble e.g. when
# buying an SSL certificate.
for domain in real_mail_domains:
aliases.add("postmaster@" + domain)
aliases.add("admin@" + domain)
return aliases
def kick(env, mail_result=None):
results = []
@@ -199,50 +342,32 @@ def kick(env, mail_result=None):
if mail_result is not None:
results.append(mail_result + "\n")
# Create hostmaster@ for the primary domain if it does not already exist.
# Default the target to administrator@ which the user is responsible for
# setting and keeping up to date.
# Ensure every required alias exists.
existing_aliases = get_mail_aliases(env)
administrator = "administrator@" + env['PRIMARY_HOSTNAME']
required_aliases = get_required_aliases(env)
def ensure_admin_alias_exists(source):
# Does this alias exists?
for s, t in existing_aliases:
if s == source:
return
# Doesn't exist.
administrator = get_system_administrator(env)
add_mail_alias(source, administrator, env, do_kick=False)
results.append("added alias %s (=> %s)\n" % (source, administrator))
ensure_admin_alias_exists("hostmaster@" + env['PRIMARY_HOSTNAME'])
# Get a list of domains we serve mail for, except ones for which the only
# email on that domain is a postmaster/admin alias to the administrator.
for alias in required_aliases:
ensure_admin_alias_exists(alias)
real_mail_domains = get_mail_domains(env,
filter_aliases = lambda alias : \
(not alias[0].startswith("postmaster@") \
and not alias[0].startswith("admin@")) \
or alias[1] != administrator \
)
# Create postmaster@ and admin@ for all domains we serve mail on.
# postmaster@ is assumed to exist by our Postfix configuration. admin@
# isn't anything, but it might save the user some trouble e.g. when
# buying an SSL certificate.
for domain in real_mail_domains:
ensure_admin_alias_exists("postmaster@" + domain)
ensure_admin_alias_exists("admin@" + domain)
# Remove auto-generated hostmaster/postmaster/admin on domains we no
# Remove auto-generated postmaster/admin on domains we no
# longer have any other email addresses for.
for source, target in existing_aliases:
user, domain = source.split("@")
if user in ("postmaster", "admin") and domain not in real_mail_domains \
and target == administrator:
if user in ("postmaster", "admin") \
and source not in required_aliases \
and target == get_system_administrator(env):
remove_mail_alias(source, env, do_kick=False)
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (source, target))