diff --git a/.gitignore b/.gitignore index bb6fa709..61a7658e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *~ tests/__pycache__/ +management/__pycache__/ +tools/__pycache__/ diff --git a/conf/management-initscript b/conf/management-initscript new file mode 100755 index 00000000..03715fdc --- /dev/null +++ b/conf/management-initscript @@ -0,0 +1,125 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: mailinabox +# Required-Start: $all +# Required-Stop: $all +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start and stop the Mail-in-a-Box management daemon. +# Description: Start and stop the Mail-in-a-Box management daemon. +### END INIT INFO + +# Adapted from http://blog.codefront.net/2007/06/11/nginx-php-and-a-php-fastcgi-daemon-init-script/ + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Mail-in-a-Box Mgmt" +NAME=mailinabox +DAEMON=/usr/bin/mailinabox-daemon +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Set defaults. +START=yes +EXEC_AS_USER=root + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# If the daemon is not enabled, give the user a warning and then exit, +# unless we are stopping the daemon +if [ "$START" != "yes" -a "$1" != "stop" ]; then + log_warning_msg "To enable $NAME, edit /etc/default/$NAME and set START=yes" + exit 0 +fi + +# Process configuration +#export ... +DAEMON_ARGS="" + + +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ + --background --make-pidfile --chuid $EXEC_AS_USER --startas $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 +} + +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE > /dev/null # --name $DAEMON + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + restart|force-reload) + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 + exit 3 + ;; +esac diff --git a/management/daemon.py b/management/daemon.py new file mode 100755 index 00000000..56849110 --- /dev/null +++ b/management/daemon.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 + +import os, os.path + +from flask import Flask, request, render_template +app = Flask(__name__) + +# Load settings from /etc/mailinabox.conf. +env = { } +for line in open("/etc/mailinabox.conf"): env.setdefault(*line.strip().split("=", 1)) +env["CONF_DIR"] = os.path.join(os.path.dirname(__file__), "../conf") + +from mailconfig import get_mail_users, add_mail_user, set_mail_password, remove_mail_user, get_mail_aliases, get_mail_domains, add_mail_alias, remove_mail_alias + +@app.route('/') +def index(): + return render_template('index.html') + +# MAIL + +@app.route('/mail/users') +def mail_users(): + return "".join(x+"\n" for x in get_mail_users(env)) + +@app.route('/mail/users/add', methods=['POST']) +def mail_users_add(): + return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), env) + +@app.route('/mail/users/password', methods=['POST']) +def mail_users_password(): + return set_mail_password(request.form.get('email', ''), request.form.get('password', ''), env) + +@app.route('/mail/users/remove', methods=['POST']) +def mail_users_remove(): + return remove_mail_user(request.form.get('email', ''), env) + +@app.route('/mail/aliases') +def mail_aliases(): + return "".join(x+"\t"+y+"\n" for x, y in get_mail_aliases(env)) + +@app.route('/mail/aliases/add', methods=['POST']) +def mail_aliases_add(): + return add_mail_alias(request.form.get('source', ''), request.form.get('destination', ''), env) + +@app.route('/mail/aliases/remove', methods=['POST']) +def mail_aliases_remove(): + return remove_mail_alias(request.form.get('source', ''), env) + +@app.route('/mail/domains') +def mail_domains(): + return "".join(x+"\n" for x in get_mail_domains(env)) + +# DNS + +@app.route('/dns/update', methods=['POST']) +def dns_update(): + from dns_update import do_dns_update + return do_dns_update(env) + +# APP + +if __name__ == '__main__': + if "DEBUG" in os.environ: app.debug = True + app.run(port=10222) diff --git a/management/dns_update.py b/management/dns_update.py new file mode 100755 index 00000000..9ac1a0f6 --- /dev/null +++ b/management/dns_update.py @@ -0,0 +1,177 @@ +# Creates DNS zone files for all of the domains of all of the mail users +# and mail aliases and restarts nsd. +######################################################################## + +import os, os.path, urllib.parse, time, re + +from mailconfig import get_mail_domains + +def do_dns_update(env): + # What domains should we serve DNS for? + domains = set() + + # Ensure the PUBLIC_HOSTNAME is in that list. + domains.add(env['PUBLIC_HOSTNAME']) + + # Add all domain names in use by email users and mail aliases. + domains |= get_mail_domains(env) + + # Make a nice and safe filename for each domain. + zonefiles = [] + for domain in domains: + zonefiles.append((domain, urllib.parse.quote(domain, safe='') + ".txt" )) + + # Write zone files. + os.makedirs('/etc/nsd/zones', exist_ok=True) + updated_domains = [] + for domain, zonefile in zonefiles: + if write_nsd_zone(domain, "/etc/nsd/zones/" + zonefile, env): + updated_domains.append(domain) + + # Write the main nsd.conf file. + write_nsd_conf(zonefiles) + + # Kick nsd. + os.system("service nsd restart") + + # Kick opendkim. + os.system("service opendkim restart") + + if len(updated_domains) == 0: updated_domains = ['(no domains required an update)'] + return "Updated: " + ",".join(updated_domains) + "\n" + +######################################################################## + +def write_nsd_zone(domain, zonefile, env): + # We set the administrative email address for every domain to domain_contact@[domain.com]. + # You should probably create an alias to your email address. + + zone = """ +$ORIGIN {domain}. ; default zone domain +$TTL 86400 ; default time to live + +@ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. ( + __SERIAL__ ; serial number + 28800 ; Refresh + 7200 ; Retry + 864000 ; Expire + 86400 ; Min TTL + ) + + NS ns1.{primary_domain}. + NS ns2.{primary_domain}. + IN A {ip} + MX 10 {primary_domain}. + + 300 TXT "v=spf1 mx -all" + +www IN A {ip} +""" + + # In PUBLIC_HOSTNAME, also define ns1 and ns2. + if domain == env["PUBLIC_HOSTNAME"]: + zone += """ +ns1 IN A {ip} +ns2 IN A {ip} +""" + + # Replace replacement strings. + zone = zone.format(domain=domain, primary_domain=env["PUBLIC_HOSTNAME"], ip=env["PUBLIC_IP"]) + + # If OpenDKIM is in use.. + opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt') + if os.path.exists(opendkim_record_file): + # Append the DKIM TXT record to the zone as generated by OpenDKIM, after string formatting above. + with open(opendkim_record_file) as orf: + zone += orf.read() + + # Append ADSP (RFC 5617) and DMARC records. + zone += """ +_adsp._domainkey IN TXT "dkim=all" +_dmarc IN TXT "v=DMARC1; p=quarantine" +""" + + # Set the serial number. + serial = time.strftime("%Y%m%d00") + if os.path.exists(zonefile): + # If the zone already exists, is different, and has a later serial number, + # increment the number. + with open(zonefile) as f: + existing_zone = f.read() + m = re.search(r"(\d+)\s*;\s*serial number", existing_zone) + if m: + existing_serial = m.group(1) + existing_zone = existing_zone.replace(m.group(0), "__SERIAL__ ; serial number") + + # If the existing zone is the same as the new zone (modulo the serial number), + # there is no need to update the file. + if zone == existing_zone: + return False + + # If the existing serial is not less than the new one, increment it. + if existing_serial >= serial: + serial = str(int(existing_serial) + 1) + + zone = zone.replace("__SERIAL__", serial) + + # Write the zone file. + with open(zonefile, "w") as f: + f.write(zone) + + return True # file is updated + +######################################################################## + +def write_nsd_conf(zonefiles): + with open("/etc/nsd/nsd.conf", "w") as f: + f.write(""" +server: + hide-version: yes + + # identify the server (CH TXT ID.SERVER entry). + identity: "" + + # The directory for zonefile: files. + zonesdir: "/etc/nsd/zones" + +# ZONES +""") + + for domain, zonefile in zonefiles: + f.write(""" +zone: + name: %s + zonefile: %s +""" % (domain, zonefile)) + +######################################################################## + +def write_opendkim_tables(zonefiles, env): + # Append a record to OpenDKIM's KeyTable and SigningTable for each domain. + # + # The SigningTable maps email addresses to signing information. The KeyTable + # maps specify the hostname, the selector, and the path to the private key. + # + # DKIM ADSP and DMARC both only support policies where the signing domain matches + # the From address, so the KeyTable must specify that the signing domain for a + # sender matches the sender's domain. + # + # In SigningTable, we map every email address to a key record named after the domain. + # Then we specify for the key record its domain, selector, and key. + + opendkim_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private') + if not os.path.exists(opendkim_key_file): return + + with open("/etc/opendkim/KeyTable", "w") as f: + f.write("\n".join( + "{domain} {domain}:mail:{key_file}".format(domain=domain, key_file=opendkim_key_file) + for domain, zonefile in zonefiles + )) + + with open("/etc/opendkim/SigningTable", "w") as f: + f.write("\n".join( + "*@{domain} {domain}".format(domain=domain) + for domain, zonefile in zonefiles + )) + + diff --git a/management/mailconfig.py b/management/mailconfig.py new file mode 100644 index 00000000..77f4312f --- /dev/null +++ b/management/mailconfig.py @@ -0,0 +1,111 @@ +import subprocess, shutil, os, sqlite3, re + +def open_database(env, with_connection=False): + conn = sqlite3.connect(env["STORAGE_ROOT"] + "/mail/users.sqlite") + if not with_connection: + return conn.cursor() + else: + return conn, conn.cursor() + +def get_mail_users(env): + c = open_database(env) + c.execute('SELECT email FROM users') + return [row[0] for row in c.fetchall()] + +def get_mail_aliases(env): + c = open_database(env) + c.execute('SELECT source, destination FROM aliases') + return [(row[0], row[1]) for row in c.fetchall()] + +def get_mail_domains(env): + def get_domain(emailaddr): + return emailaddr.split('@', 1)[1] + return set([get_domain(addr) for addr in get_mail_users(env)] + [get_domain(addr1) for addr1, addr2 in get_mail_aliases(env)]) + +def add_mail_user(email, pw, env): + if not re.match("\w[\w\.]+@\w[\w\.]+$", email): + return ("Invalid email address.", 400) + + # get the database + conn, c = open_database(env, with_connection=True) + + # hash the password + pw = subprocess.check_output(["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip() + + # add the user to the database + try: + c.execute("INSERT INTO users (email, password) VALUES (?, ?)", (email, pw)) + except sqlite3.IntegrityError: + return ("User already exists.", 400) + + # write databasebefore next step + conn.commit() + + # Create the user's INBOX and Spam folders and subscribe them. + + # 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. + existing_mboxes = subprocess.check_output(["doveadm", "mailbox", "list", "-u", email, "-8"]).decode("utf8").split("\n") + + if "INBOX" not in existing_mboxes: subprocess.check_call(["doveadm", "mailbox", "create", "-u", email, "-s", "INBOX"]) + if "Spam" not in existing_mboxes: subprocess.check_call(["doveadm", "mailbox", "create", "-u", email, "-s", "Spam"]) + + # Create the user's sieve script to move spam into the Spam folder, and make it owned by mail. + maildirstat = os.stat(env["STORAGE_ROOT"] + "/mail/mailboxes") + (em_user, em_domain) = email.split("@", 1) + user_mail_dir = env["STORAGE_ROOT"] + ("/mail/mailboxes/%s/%s" % (em_domain, em_user)) + if not os.path.exists(user_mail_dir): + os.makedirs(user_mail_dir) + os.chown(user_mail_dir, maildirstat.st_uid, maildirstat.st_gid) + shutil.copyfile(env["CONF_DIR"] + "/dovecot_sieve.txt", user_mail_dir + "/.dovecot.sieve") + os.chown(user_mail_dir + "/.dovecot.sieve", maildirstat.st_uid, maildirstat.st_gid) + + # Update DNS in case any new domains are added. + from dns_update import do_dns_update + return do_dns_update(env) + +def set_mail_password(email, pw, env): + # hash the password + pw = subprocess.check_output(["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip() + + # update the database + conn, c = open_database(env, with_connection=True) + c.execute("UPDATE users SET password=? WHERE email=?", (pw, email)) + if c.rowcount != 1: + return ("That's not a user (%s)." % email, 400) + conn.commit() + return "OK" + +def remove_mail_user(email, env): + conn, c = open_database(env, with_connection=True) + c.execute("DELETE FROM users WHERE email=?", (email,)) + if c.rowcount != 1: + return ("That's not a user (%s)." % email, 400) + conn.commit() + + # Update DNS in case any domains are removed. + from dns_update import do_dns_update + return do_dns_update(env) + +def add_mail_alias(source, destination, env): + conn, c = open_database(env, with_connection=True) + try: + c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (source, destination)) + except sqlite3.IntegrityError: + return ("Alias already exists (%s)." % source, 400) + conn.commit() + + # Update DNS in case any new domains are added. + from dns_update import do_dns_update + return do_dns_update(env) + +def remove_mail_alias(source, env): + conn, c = open_database(env, with_connection=True) + c.execute("DELETE FROM aliases WHERE source=?", (source,)) + if c.rowcount != 1: + return ("That's not an alias (%s)." % source, 400) + conn.commit() + + # Update DNS in case any domains are removed. + from dns_update import do_dns_update + return do_dns_update(env) diff --git a/management/templates/index.html b/management/templates/index.html new file mode 100644 index 00000000..925201e3 --- /dev/null +++ b/management/templates/index.html @@ -0,0 +1,11 @@ + + + + Mail-in-a-Box Management Server + + +

Mail-in-a-Box Management Server

+ +

Use this server to issue commands to the Mail-in-a-Box management daemon.

+ + \ No newline at end of file diff --git a/setup/dns_update.sh b/setup/dns_update.sh deleted file mode 100755 index bac81806..00000000 --- a/setup/dns_update.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# DNS: Creates DNS zone files -############################# - -# Create nsd.conf and zone files, and updates the OpenDKIM signing tables. - -# We set the administrative email address for every domain to domain_contact@[domain.com]. -# You should probably create an alias to your email address. - -# This script is safe to run on its own. - -source /etc/mailinabox.conf # load global vars - -# Ensure a zone file exists for every domain name in use by a mail user. -for mail_user in `tools/mail.py user`; do - domain=`echo $mail_user | sed s/.*@//` - if [ ! -f $STORAGE_ROOT/dns/$domain.txt ]; then - echo "" > $STORAGE_ROOT/dns/$domain.txt; - fi -done - -# Create the top of nsd.conf. - -cat > /etc/nsd/nsd.conf << EOF; -server: - hide-version: yes - - # identify the server (CH TXT ID.SERVER entry). - identity: "" - - # The directory for zonefile: files. - zonesdir: "/etc/nsd/zones" - -# ZONES -EOF - -# For every zone file in our dns directory, build a proper zone -# file and mention it in nsd.conf. And add information to the -# OpenDKIM signing tables. - -mkdir -p /etc/nsd/zones; - -truncate --size 0 /etc/opendkim/KeyTable -truncate --size 0 /etc/opendkim/SigningTable - -for fn in $STORAGE_ROOT/dns/*.txt; do - # $fn is the zone configuration file, which is just a placeholder now. - # For every file like mydomain.com.txt we'll create zone information - # for that domain. We don't actually read the file. - # $fn2 is the file without the directory. - # $zone is the domain name (just mydomain.com). - fn2=`basename $fn` - zone=`echo $fn2 | sed "s/.txt\$//"` - - # If the zone file exists, get the existing zone serial number so we can increment it. - # TODO: This needs to be done better so that the existing serial number is persisted in the storage area. - serial=`date +"%Y%m%d00"` - if [ -f /etc/nsd/zones/$fn2 ]; then - existing_serial=`grep "serial number" /etc/nsd/zones/$fn2 | sed "s/; serial number//"` - if [ ! -z "$existing_serial" ]; then - serial=`echo $existing_serial + 1 | bc` - fi - fi - - # Create the zone file. - cat > /etc/nsd/zones/$fn2 << EOF; -\$ORIGIN $zone. ; default zone domain -\$TTL 86400 ; default time to live - -@ IN SOA ns1.$PUBLIC_HOSTNAME. hostmaster.$PUBLIC_HOSTNAME. ( - $serial ; serial number - 28800 ; Refresh - 7200 ; Retry - 864000 ; Expire - 86400 ; Min TTL - ) - - NS ns1.$PUBLIC_HOSTNAME. - NS ns2.$PUBLIC_HOSTNAME. - IN A $PUBLIC_IP - MX 10 $PUBLIC_HOSTNAME. - - 300 TXT "v=spf1 mx -all" - -www IN A $PUBLIC_IP -EOF - - # In PUBLIC_HOSTNAME, also define ns1 and ns2. - if [ "$zone" = $PUBLIC_HOSTNAME ]; then - cat >> /etc/nsd/zones/$fn2 << EOF; -ns1 IN A $PUBLIC_IP -ns2 IN A $PUBLIC_IP -EOF - fi - - # If OpenDKIM is set up.. - if [ -f "$STORAGE_ROOT/mail/dkim/mail.txt" ]; then - # Append the DKIM TXT record to the zone as generated by OpenDKIM. - cat "$STORAGE_ROOT/mail/dkim/mail.txt" >> /etc/nsd/zones/$fn2; - - # Append ADSP (RFC 5617) and DMARC records. - cat >> /etc/nsd/zones/$fn2 << EOF; -_adsp._domainkey IN TXT "dkim=all" -_dmarc IN TXT "v=DMARC1; p=quarantine" -EOF - fi - - # Add this zone file to the main nsd configuration file. - cat >> /etc/nsd/nsd.conf << EOF; -zone: - name: $zone - zonefile: $fn2 -EOF - - # Append a record to OpenDKIM's KeyTable and SigningTable. The SigningTable maps - # email addresses to signing information. The KeyTable maps specify the hostname, - # the selector, and the path to the private key. - # - # DKIM ADSP and DMARC both only support policies where the signing domain matches - # the From address, so the KeyTable must specify that the signing domain for a - # sender matches the sender's domain. - # - # In SigningTable, we map every email address to a key record called $zone. - # Then we specify for the key record named $zone its domain, selector, and key. - echo "$zone $zone:mail:$STORAGE_ROOT/mail/dkim/mail.private" >> /etc/opendkim/KeyTable - echo "*@$zone $zone" >> /etc/opendkim/SigningTable - -done - -# Kick nsd. -service nsd restart - -# Kick opendkim. -service opendkim restart - diff --git a/setup/management.sh b/setup/management.sh new file mode 100755 index 00000000..ea557708 --- /dev/null +++ b/setup/management.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +source setup/functions.sh + +apt_install python3-flask links + +# Link the management server daemon into a well known location. +rm -f /usr/bin/mailinabox-daemon +ln -s `pwd`/management/daemon.py /usr/bin/mailinabox-daemon + +# Create an init script to start the management daemon and keep it +# running after a reboot. +rm -f /etc/init.d/mailinabox +ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox +update-rc.d mailinabox defaults + +# Start it. +service mailinabox restart diff --git a/setup/start.sh b/setup/start.sh index a62e58f2..a6e3e384 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -69,9 +69,13 @@ fi . setup/mail.sh . setup/dkim.sh . setup/spamassassin.sh -. setup/dns_update.sh . setup/web.sh . setup/webmail.sh +. setup/management.sh + +# Write the DNS configuration files. +sleep 2 # wait for the daemon to start +curl -d POSTDATA http://127.0.0.1:10222/dns/update if [ -t 0 ]; then # are we in an interactive shell? if [ -z "`tools/mail.py user`" ]; then @@ -85,3 +89,4 @@ if [ -z "`tools/mail.py user`" ]; then tools/mail.py alias add postmaster@$PUBLIC_HOSTNAME $EMAIL_ADDR fi fi + diff --git a/tools/mail.py b/tools/mail.py index dbd0a13e..b1a175e8 100755 --- a/tools/mail.py +++ b/tools/mail.py @@ -1,14 +1,15 @@ #!/usr/bin/python3 -import sys, sqlite3, subprocess, shutil, os +import sys, urllib.request, urllib.error -# Load STORAGE_ROOT setting from /etc/mailinabox.conf. -env = { } -for line in open("/etc/mailinabox.conf"): env.setdefault(*line.strip().split("=", 1)) - -# Connect to database. -conn = sqlite3.connect(env["STORAGE_ROOT"] + "/mail/users.sqlite") -c = conn.cursor() +def mgmt(cmd, data=None): + req = urllib.request.Request('http://localhost:10222' + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None) + try: + response = urllib.request.urlopen(req) + except urllib.error.HTTPError as e: + print(e.read().decode('utf8')) + sys.exit(1) + return response.read().decode('utf8') if len(sys.argv) < 2: print("Usage: ") @@ -24,9 +25,7 @@ if len(sys.argv) < 2: print() elif sys.argv[1] == "user" and len(sys.argv) == 2: - c.execute('SELECT email FROM users') - for row in c.fetchall(): - print(row[0]) + print(mgmt("/mail/users")) elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"): if len(sys.argv) < 5: @@ -38,68 +37,23 @@ elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"): else: email, pw = sys.argv[3:5] - # hash the password - pw = subprocess.check_output(["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip() - if sys.argv[2] == "add": - try: - c.execute("INSERT INTO users (email, password) VALUES (?, ?)", (email, pw)) - except sqlite3.IntegrityError: - print("User already exists.") - sys.exit(1) - - conn.commit() # write it before next step - - # Create the user's INBOX and Spam folders and subscribe them. - - # 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. - existing_mboxes = subprocess.check_output(["doveadm", "mailbox", "list", "-u", email, "-8"]).decode("utf8").split("\n") - - if "INBOX" not in existing_mboxes: subprocess.check_call(["doveadm", "mailbox", "create", "-u", email, "-s", "INBOX"]) - if "Spam" not in existing_mboxes: subprocess.check_call(["doveadm", "mailbox", "create", "-u", email, "-s", "Spam"]) - - # Create the user's sieve script to move spam into the Spam folder, and make it owned by mail. - maildirstat = os.stat(env["STORAGE_ROOT"] + "/mail/mailboxes") - (em_user, em_domain) = email.split("@", 1) - user_mail_dir = env["STORAGE_ROOT"] + ("/mail/mailboxes/%s/%s" % (em_domain, em_user)) - if not os.path.exists(user_mail_dir): - os.makedirs(user_mail_dir) - os.chown(user_mail_dir, maildirstat.st_uid, maildirstat.st_gid) - shutil.copyfile("conf/dovecot_sieve.txt", user_mail_dir + "/.dovecot.sieve") - os.chown(user_mail_dir + "/.dovecot.sieve", maildirstat.st_uid, maildirstat.st_gid) - + print(mgmt("/mail/users/add", { "email": email, "password": pw })) elif sys.argv[2] == "password": - c.execute("UPDATE users SET password=? WHERE email=?", (pw, email)) - if c.rowcount != 1: - print("That's not a user.") - sys.exit(1) + print(mgmt("/mail/users/password", { "email": email, "password": pw })) elif sys.argv[1] == "user" and sys.argv[2] == "remove" and len(sys.argv) == 4: - c.execute("DELETE FROM users WHERE email=?", (sys.argv[3],)) - if c.rowcount != 1: - print("That's not a user.") - sys.exit(1) + print(mgmt("/mail/users/remove", { "email": sys.argv[3] })) elif sys.argv[1] == "alias" and len(sys.argv) == 2: - c.execute('SELECT source, destination FROM aliases') - for row in c.fetchall(): - print(row[0], "=>", row[1]) + print(mgmt("/mail/aliases")) elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5: - try: - c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (sys.argv[3], sys.argv[4])) - except sqlite3.IntegrityError: - print("Alias already exists.") - sys.exit(1) + print(mgmt("/mail/aliases/add", { "source": sys.argv[3], "destination": sys.argv[4] })) elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4: - c.execute("DELETE FROM aliases WHERE source=?", (sys.argv[3],)) - if c.rowcount != 1: - print("That's not an alias.") - sys.exit(1) + print(mgmt("/mail/aliases/remove", { "source": sys.argv[3] })) else: print("Invalid command-line arguments.") -conn.commit()