From 1795f8aefdd91a1e798242574b0fc48a459c363f Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 27 Apr 2024 18:41:35 -0400 Subject: [PATCH 01/55] bringing in quota changes --- conf/dovecot/conf.d/15-mailboxes.conf | 63 +++++++++++++ management/cli.py | 20 +++++ management/daemon.py | 49 ++++++++++- management/mailconfig.py | 122 ++++++++++++++++++++++++-- management/status_checks.py | 4 +- management/templates/users.html | 106 ++++++++++++++++++++-- setup/bootstrap.sh | 25 +++--- setup/mail-dovecot.sh | 48 +++++++--- setup/mail-postfix.sh | 24 ++--- setup/mail-users.sh | 19 ++-- setup/webmail.sh | 29 +++--- 11 files changed, 436 insertions(+), 73 deletions(-) create mode 100644 conf/dovecot/conf.d/15-mailboxes.conf mode change 100755 => 100644 setup/webmail.sh diff --git a/conf/dovecot/conf.d/15-mailboxes.conf b/conf/dovecot/conf.d/15-mailboxes.conf new file mode 100644 index 00000000..58e2efed --- /dev/null +++ b/conf/dovecot/conf.d/15-mailboxes.conf @@ -0,0 +1,63 @@ +## NOTE: This file is automatically generated by Mail-in-a-Box. +## Do not edit this file. It is continually updated by +## Mail-in-a-Box and your changes will be lost. +## +## Mail-in-a-Box machines are not meant to be modified. +## If you modify any system configuration you are on +## your own --- please do not ask for help from us. + +namespace inbox { + # Automatically create & subscribe some folders. + # * Create and subscribe the INBOX folder. + # * Our sieve rule for spam expects that the Spam folder exists. + # * Z-Push must be configured with the same settings in conf/zpush/backend_imap.php (#580). + + # MUA notes: + # * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359). + # * K-9 mail will poll every 90 seconds if a Drafts folder does not exist. + # * Apple's OS X Mail app will create 'Sent Messages' if it doesn't see a folder with the \Sent flag (#571, #573) and won't be able to archive messages unless 'Archive' exists (#581). + # * Thunderbird's default in its UI is 'Archives' (plural) but it will configure new accounts to use whatever we say here (#581). + + # auto: + # 'create' will automatically create this mailbox. + # 'subscribe' will both create and subscribe to the mailbox. + + # special_use is a space separated list of IMAP SPECIAL-USE + # attributes as specified by RFC 6154: + # \All \Archive \Drafts \Flagged \Junk \Sent \Trash + + mailbox INBOX { + auto = subscribe + } + mailbox Spam { + special_use = \Junk + auto = subscribe + } + mailbox Drafts { + special_use = \Drafts + auto = subscribe + } + mailbox Sent { + special_use = \Sent + auto = subscribe + } + mailbox Trash { + special_use = \Trash + auto = subscribe + } + mailbox Archive { + special_use = \Archive + auto = subscribe + } + + # dovevot's standard mailboxes configuration file marks two sent folders + # with the \Sent attribute, just in case clients don't agree about which + # they're using. We'll keep that, plus add Junk as an alterative for Spam. + # These are not auto-created. + mailbox "Sent Messages" { + special_use = \Sent + } + mailbox Junk { + special_use = \Junk + } +} diff --git a/management/cli.py b/management/cli.py index 70dbe894..1c3fa4bd 100755 --- a/management/cli.py +++ b/management/cli.py @@ -60,11 +60,13 @@ def setup_key_auth(mgmt_uri): if len(sys.argv) < 2: print("""Usage: + {cli} system default-quota [new default] (set default quota for system) {cli} user (lists users) {cli} user add user@domain.com [password] {cli} user password user@domain.com [password] {cli} user remove user@domain.com {cli} user make-admin user@domain.com + {cli} user quota user@domain [new-quota] {cli} user remove-admin user@domain.com {cli} user admins (lists admins) {cli} user mfa show user@domain.com (shows MFA devices for user, if any) @@ -88,6 +90,10 @@ elif sys.argv[1] == "user" and len(sys.argv) == 2: print(user['email'], end='') if "admin" in user['privileges']: print("*", end='') + if user['quota'] == '0': + print(" unlimited", end='') + else: + print(" " + user['quota'], end='') print() elif sys.argv[1] == "user" and sys.argv[2] in {"add", "password"}: @@ -117,6 +123,14 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins": if "admin" in user['privileges']: print(user['email']) +elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4: + # Set a user's quota + print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3])) + +elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5: + # Set a user's quota + users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }) + elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]: # Show MFA status for a user. status = mgmt("/mfa/status", { "user": sys.argv[4] }, is_json=True) @@ -138,6 +152,12 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5: elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4: print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] })) +elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 3: + print(mgmt("/system/default-quota?text=1")) + +elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 4: + print(mgmt("/system/default-quota", { "default_quota": sys.argv[3]})) + else: print("Invalid command-line arguments.") sys.exit(1) diff --git a/management/daemon.py b/management/daemon.py index 3aa6eed2..e850e6c6 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -21,6 +21,7 @@ import auth, utils from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user 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_quota, set_mail_quota, get_default_quota, validate_quota from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa import contextlib @@ -191,8 +192,31 @@ def mail_users(): @app.route('/mail/users/add', methods=['POST']) @authorized_personnel_only def mail_users_add(): + quota = request.form.get('quota', get_default_quota(env)) try: - return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), env) + return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, env) + except ValueError as e: + return (str(e), 400) + +@app.route('/mail/users/quota', methods=['GET']) +@authorized_personnel_only +def get_mail_users_quota(): + email = request.values.get('email', '') + quota = get_mail_quota(email, env) + + if request.values.get('text'): + return quota + + return json_response({ + "email": email, + "quota": quota + }) + +@app.route('/mail/users/quota', methods=['POST']) +@authorized_personnel_only +def mail_users_quota(): + try: + return set_mail_quota(request.form.get('email', ''), request.form.get('quota'), env) except ValueError as e: return (str(e), 400) @@ -651,6 +675,29 @@ def privacy_status_set(): utils.write_settings(config, env) return "OK" +@app.route('/system/default-quota', methods=["GET"]) +@authorized_personnel_only +def default_quota_get(): + if request.values.get('text'): + return get_default_quota(env) + else: + return json_response({ + "default-quota": get_default_quota(env), + }) + +@app.route('/system/default-quota', methods=["POST"]) +@authorized_personnel_only +def default_quota_set(): + config = utils.load_settings(env) + try: + config["default-quota"] = validate_quota(request.values.get('default_quota')) + utils.write_settings(config, env) + + except ValueError as e: + return ("ERROR: %s" % str(e), 400) + + return "OK" + # MUNIN @app.route('/munin/') diff --git a/management/mailconfig.py b/management/mailconfig.py index 39ca1b6e..f9c8f237 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -102,6 +102,18 @@ def get_mail_users(env): users = [ row[0] for row in c.fetchall() ] return utils.sort_email_addresses(users, env) +def sizeof_fmt(num): + for unit in ['','K','M','G','T']: + if abs(num) < 1024.0: + if abs(num) > 99: + return "%3.0f%s" % (num, unit) + else: + return "%2.1f%s" % (num, unit) + + num /= 1024.0 + + return str(num) + def get_mail_users_ex(env, with_archived=False): # Returns a complex data structure of all user accounts, optionally # including archived (status="inactive") accounts. @@ -125,13 +137,46 @@ def get_mail_users_ex(env, with_archived=False): users = [] active_accounts = set() c = open_database(env) - c.execute('SELECT email, privileges FROM users') - for email, privileges in c.fetchall(): + c.execute('SELECT email, privileges, quota FROM users') + for email, privileges, quota in c.fetchall(): active_accounts.add(email) + (user, domain) = email.split('@') + box_size = 0 + box_count = 0 + box_quota = 0 + percent = '' + try: + dirsize_file = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes/%s/%s/maildirsize' % (domain, user)) + with open(dirsize_file, 'r') as f: + box_quota = int(f.readline().split('S')[0]) + for line in f.readlines(): + (size, count) = line.split(' ') + box_size += int(size) + box_count += int(count) + + try: + percent = (box_size / box_quota) * 100 + except: + percent = 'Error' + + except: + box_size = '?' + box_count = '?' + box_quota = '?' + percent = '?' + + if quota == '0': + percent = '' + user = { "email": email, "privileges": parse_privs(privileges), + "quota": quota, + "box_quota": box_quota, + "box_size": sizeof_fmt(box_size) if box_size != '?' else box_size, + "percent": '%3.0f%%' % percent if type(percent) != str else percent, + "box_count": box_count, "status": "active", } users.append(user) @@ -150,6 +195,10 @@ def get_mail_users_ex(env, with_archived=False): "privileges": [], "status": "inactive", "mailbox": mbox, + "box_count": '?', + "box_size": '?', + "box_quota": '?', + "percent": '?', } users.append(user) @@ -266,7 +315,7 @@ def get_mail_domains(env, filter_aliases=lambda alias : True, users_only=False): domains.extend([get_domain(address, as_unicode=False) for address, _, _, auto in get_mail_aliases(env) if filter_aliases(address) and not auto ]) return set(domains) -def add_mail_user(email, pw, privs, env): +def add_mail_user(email, pw, privs, quota, env): # validate email if email.strip() == "": return ("No email address provided.", 400) @@ -292,6 +341,14 @@ def add_mail_user(email, pw, privs, env): validation = validate_privilege(p) if validation: return validation + if quota is None: + quota = get_default_quota() + + try: + quota = validate_quota(quota) + except ValueError as e: + return (str(e), 400) + # get the database conn, c = open_database(env, with_connection=True) @@ -300,14 +357,16 @@ def add_mail_user(email, pw, privs, env): # add the user to the database try: - c.execute("INSERT INTO users (email, password, privileges) VALUES (?, ?, ?)", - (email, pw, "\n".join(privs))) + c.execute("INSERT INTO users (email, password, privileges, quota) VALUES (?, ?, ?, ?)", + (email, pw, "\n".join(privs), quota)) except sqlite3.IntegrityError: return ("User already exists.", 400) # write databasebefore next step conn.commit() + dovecot_quota_recalc(email) + # Update things in case any new domains are added. return kick(env, "mail user added") @@ -332,6 +391,59 @@ def hash_password(pw): # http://wiki2.dovecot.org/Authentication/PasswordSchemes return utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip() + +def get_mail_quota(email, env): + conn, c = open_database(env, with_connection=True) + c.execute("SELECT quota FROM users WHERE email=?", (email,)) + rows = c.fetchall() + if len(rows) != 1: + return ("That's not a user (%s)." % email, 400) + + return rows[0][0] + + +def set_mail_quota(email, quota, env): + # validate that password is acceptable + quota = validate_quota(quota) + + # update the database + conn, c = open_database(env, with_connection=True) + c.execute("UPDATE users SET quota=? WHERE email=?", (quota, email)) + if c.rowcount != 1: + return ("That's not a user (%s)." % email, 400) + conn.commit() + + dovecot_quota_recalc(email) + + return "OK" + +def dovecot_quota_recalc(email): + # dovecot processes running for the user will not recognize the new quota setting + # a reload is necessary to reread the quota setting, but it will also shut down + # running dovecot processes. Email clients generally log back in when they lose + # a connection. + # subprocess.call(['doveadm', 'reload']) + + # force dovecot to recalculate the quota info for the user. + subprocess.call(["doveadm", "quota", "recalc", "-u", email]) + +def get_default_quota(env): + config = utils.load_settings(env) + return config.get("default-quota", '0') + +def validate_quota(quota): + # validate quota + quota = quota.strip().upper() + + if quota == "": + raise ValueError("No quota provided.") + if re.search(r"[\s,.]", quota): + raise ValueError("Quotas cannot contain spaces, commas, or decimal points.") + if not re.match(r'^[\d]+[GM]?$', quota): + raise ValueError("Invalid quota.") + + return quota + def get_mail_password(email, env): # Gets the hashed password for a user. Passwords are stored in Dovecot's # password format, with a prefixed scheme. diff --git a/management/status_checks.py b/management/status_checks.py index 51f8e631..0ba9c3c5 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -282,7 +282,7 @@ def run_network_checks(env, output): # The user might have ended up on an IP address that was previously in use # by a spammer, or the user may be deploying on a residential network. We # will not be able to reliably send mail in these cases. - + # See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for # information on spamhaus return codes rev_ip4 = ".".join(reversed(env['PUBLIC_IP'].split('.'))) @@ -721,7 +721,7 @@ def check_mail_domain(domain, env, output): # 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 # and will not be able to reliably send mail. - + # See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for # information on spamhaus return codes dbl = query_dns(domain+'.dbl.spamhaus.org', "A", nxdomain=None) diff --git a/management/templates/users.html b/management/templates/users.html index 4924c7c8..da04fcc9 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -6,6 +6,7 @@ #user_table .account_inactive .if_active { display: none; } #user_table .account_active .if_inactive { display: none; } #user_table .account_active.if_inactive { display: none; } +.row-center { text-align: center; }

Add a mail user

@@ -27,6 +28,10 @@ +
+ + +

Existing mail users

- + + + + + @@ -53,10 +63,22 @@ + + + +
Email AddressEmail AddressMessagesSizeUsedQuota Actions
+ + + + set quota + + | + + set password @@ -97,10 +119,28 @@ - - + + + + + + + + + + + + + + + + + + + +
Verb Action
GET(none) Returns a list of existing mail users. Adding ?format=json to the URL will give JSON-encoded results.
POST/add Adds a new mail user. Required POST-body parameters are email and password.
POST/remove Removes a mail user. Required POST-body parameter is email.
POST/addAdds a new mail user. Required POST-body parameters are email and password. Optional parameters: privilege=admin and quota
POST/removeRemoves a mail user. Required POST-by parameter is email.
POST/privileges/add Used to make a mail user an admin. Required POST-body parameters are email and privilege=admin.
POST/privileges/remove Used to remove the admin privilege from a mail user. Required POST-body parameter is email.
GET/quotaGet the quota for a mail user. Required POST-body parameters are email and will return JSON result
POST/quotaSet the quota for a mail user. Required POST-body parameters are email and quota.

Examples:

@@ -125,6 +165,15 @@ curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/us diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index de373aff..be47b18c 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -51,9 +51,9 @@ if [[ $EUID -ne 0 ]]; then fi # Clone the Mail-in-a-Box repository if it doesn't exist. -if [ ! -d $HOME/mailinabox ]; then +if [ ! -d "$HOME/mailinabox" ]; then if [ ! -f /usr/bin/git ]; then - echo Installing git . . . + echo "Installing git . . ." apt-get -q -q update DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null echo @@ -63,25 +63,25 @@ if [ ! -d $HOME/mailinabox ]; then SOURCE=https://github.com/mail-in-a-box/mailinabox fi - echo Downloading Mail-in-a-Box $TAG. . . + echo "Downloading Mail-in-a-Box $TAG. . ." git clone \ - -b $TAG --depth 1 \ - $SOURCE \ - $HOME/mailinabox \ + -b "$TAG" --depth 1 \ + "$SOURCE" \ + "$HOME/mailinabox" \ < /dev/null 2> /dev/null echo fi # Change directory to it. -cd $HOME/mailinabox +cd "$HOME/mailinabox" || exit # Update it. -if [ "$TAG" != $(git describe --always) ]; then - echo Updating Mail-in-a-Box to $TAG . . . - git fetch --depth 1 --force --prune origin tag $TAG - if ! git checkout -q $TAG; then - echo "Update failed. Did you modify something in $(pwd)?" +if [ "$TAG" != "$(git describe --always)" ]; then + echo "Updating Mail-in-a-Box to $TAG . . ." + git fetch --depth 1 --force --prune origin tag "$TAG" + if ! git checkout -q "$TAG"; then + echo "Update failed. Did you modify something in $PWD?" exit 1 fi echo @@ -89,4 +89,3 @@ fi # Start setup script. setup/start.sh - diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 8d45a50b..1a9fb1fc 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -45,8 +45,8 @@ apt_install \ # - https://www.dovecot.org/list/dovecot/2012-August/137569.html # - https://www.dovecot.org/list/dovecot/2011-December/132455.html tools/editconf.py /etc/dovecot/conf.d/10-master.conf \ - default_process_limit=$(echo "$(nproc) * 250" | bc) \ - default_vsz_limit=$(echo "$(free -tm | tail -1 | awk '{print $2}') / 3" | bc)M \ + default_process_limit="$(($(nproc) * 250))" \ + default_vsz_limit="$(($(free -tm | tail -1 | awk '{print $2}') / 3))M" \ log_path=/var/log/mail.log # The inotify `max_user_instances` default is 128, which constrains @@ -61,12 +61,38 @@ tools/editconf.py /etc/sysctl.conf \ # username part of the user's email address. We'll ensure that no bad domains or email addresses # are created within the management daemon. tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ - mail_location=maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n \ + mail_location="maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n" \ mail_privileged_group=mail \ first_valid_uid=0 # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. -cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf +cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ +sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then + sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf +fi + +# configure stuff for quota support +if ! grep -q "quota_status_success = DUNNO" /etc/dovecot/conf.d/90-quota.conf; then + cat > /etc/dovecot/conf.d/90-quota.conf << EOF; +plugin { + quota = maildir + + quota_grace = 10% + + quota_status_success = DUNNO + quota_status_nouser = DUNNO + quota_status_overquota = "522 5.2.2 Mailbox is full" +} + +service quota-status { + executable = quota-status -p postfix + inet_listener { + port = 12340 + } +} +EOF +fi # ### IMAP/POP @@ -152,7 +178,7 @@ EOF # Setting a `postmaster_address` is required or LMTP won't start. An alias # will be created automatically by our management daemon. tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \ - postmaster_address=postmaster@$PRIMARY_HOSTNAME + "postmaster_address=postmaster@$PRIMARY_HOSTNAME" # ### Sieve @@ -201,14 +227,14 @@ chown -R mail:dovecot /etc/dovecot chmod -R o-rwx /etc/dovecot # Ensure mailbox files have a directory that exists and are owned by the mail user. -mkdir -p $STORAGE_ROOT/mail/mailboxes -chown -R mail:mail $STORAGE_ROOT/mail/mailboxes +mkdir -p "$STORAGE_ROOT/mail/mailboxes" +chown -R mail:mail "$STORAGE_ROOT/mail/mailboxes" # Same for the sieve scripts. -mkdir -p $STORAGE_ROOT/mail/sieve -mkdir -p $STORAGE_ROOT/mail/sieve/global_before -mkdir -p $STORAGE_ROOT/mail/sieve/global_after -chown -R mail:mail $STORAGE_ROOT/mail/sieve +mkdir -p "$STORAGE_ROOT/mail/sieve" +mkdir -p "$STORAGE_ROOT/mail/sieve/global_before" +mkdir -p "$STORAGE_ROOT/mail/sieve/global_after" +chown -R mail:mail "$STORAGE_ROOT/mail/sieve" # Allow the IMAP/POP ports in the firewall. ufw_allow imaps diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 24969513..6653f061 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -55,9 +55,9 @@ apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates # * Set the SMTP banner (which must have the hostname first, then anything). tools/editconf.py /etc/postfix/main.cf \ inet_interfaces=all \ - smtp_bind_address=$PRIVATE_IP \ - smtp_bind_address6=$PRIVATE_IPV6 \ - myhostname=$PRIMARY_HOSTNAME\ + smtp_bind_address="$PRIVATE_IP" \ + smtp_bind_address6="$PRIVATE_IPV6" \ + myhostname="$PRIMARY_HOSTNAME"\ smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \ mydestination=localhost @@ -138,9 +138,9 @@ sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters tools/editconf.py /etc/postfix/main.cf \ smtpd_tls_security_level=may\ smtpd_tls_auth_only=yes \ - smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \ - smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \ - smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \ + smtpd_tls_cert_file="$STORAGE_ROOT/ssl/ssl_certificate.pem" \ + smtpd_tls_key_file="$STORAGE_ROOT/ssl/ssl_private_key.pem" \ + smtpd_tls_dh1024_param_file="$STORAGE_ROOT/ssl/dh2048.pem" \ smtpd_tls_protocols="!SSLv2,!SSLv3" \ smtpd_tls_ciphers=medium \ tls_medium_cipherlist=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA \ @@ -238,7 +238,7 @@ tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit= # "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC tools/editconf.py /etc/postfix/main.cf \ smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org=127.0.1.[2..99]" \ - smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023" + smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023,check_policy_service inet:127.0.0.1:12340" # Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that # Postgrey listens on the same interface (and not IPv6, for instance). @@ -260,17 +260,17 @@ tools/editconf.py /etc/default/postgrey \ # If the $STORAGE_ROOT/mail/postgrey is empty, copy the postgrey database over from the old location -if [ ! -d $STORAGE_ROOT/mail/postgrey/db ]; then +if [ ! -d "$STORAGE_ROOT/mail/postgrey/db" ]; then # Stop the service service postgrey stop # Ensure the new paths for postgrey db exists - mkdir -p $STORAGE_ROOT/mail/postgrey/db + mkdir -p "$STORAGE_ROOT/mail/postgrey/db" # Move over database files - mv /var/lib/postgrey/* $STORAGE_ROOT/mail/postgrey/db/ || true + mv /var/lib/postgrey/* "$STORAGE_ROOT/mail/postgrey/db/" || true fi # Ensure permissions are set -chown -R postgrey:postgrey $STORAGE_ROOT/mail/postgrey/ -chmod 700 $STORAGE_ROOT/mail/postgrey/{,db} +chown -R postgrey:postgrey "$STORAGE_ROOT/mail/postgrey/" +chmod 700 "$STORAGE_ROOT/mail/postgrey/"{,db} # We are going to setup a newer whitelist for postgrey, the version included in the distribution is old cat > /etc/cron.daily/mailinabox-postgrey-whitelist << EOF; diff --git a/setup/mail-users.sh b/setup/mail-users.sh index b570f037..99dcea37 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -18,12 +18,14 @@ source /etc/mailinabox.conf # load global vars db_path=$STORAGE_ROOT/mail/users.sqlite # Create an empty database if it doesn't yet exist. -if [ ! -f $db_path ]; then - echo Creating new user database: $db_path; - echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path; - echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path; - echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path; - echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path; +if [ ! -f "$db_path" ]; then + echo "Creating new user database: $db_path"; + echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', quota TEXT NOT NULL DEFAULT '0');" | sqlite3 $db_path; + echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; + echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 "$db_path"; + echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; +elif sqlite3 $db_path ".schema users" | grep --invert-match quota; then + echo "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';" | sqlite3 $db_path; fi # ### User Authentication @@ -51,7 +53,7 @@ driver = sqlite connect = $db_path default_pass_scheme = SHA512-CRYPT password_query = SELECT email as user, password FROM users WHERE email='%u'; -user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users WHERE email='%u'; +user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home, '*:bytes=' || quota AS quota_rule FROM users WHERE email='%u'; iterate_query = SELECT email AS user FROM users; EOF chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions @@ -159,4 +161,5 @@ EOF restart_service postfix restart_service dovecot - +# force a recalculation of all user quotas +doveadm quota recalc -A diff --git a/setup/webmail.sh b/setup/webmail.sh old mode 100755 new mode 100644 index 3cff1416..c6380bd5 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -22,8 +22,8 @@ source /etc/mailinabox.conf # load global vars echo "Installing Roundcube (webmail)..." apt_install \ dbconfig-common \ - php${PHP_VER}-cli php${PHP_VER}-sqlite3 php${PHP_VER}-intl php${PHP_VER}-common php${PHP_VER}-curl php${PHP_VER}-imap \ - php${PHP_VER}-gd php${PHP_VER}-pspell php${PHP_VER}-mbstring libjs-jquery libjs-jquery-mousewheel libmagic1 \ + php"${PHP_VER}"-cli php"${PHP_VER}"-sqlite3 php"${PHP_VER}"-intl php"${PHP_VER}"-common php"${PHP_VER}"-curl php"${PHP_VER}"-imap \ + php"${PHP_VER}"-gd php"${PHP_VER}"-pspell php"${PHP_VER}"-mbstring libjs-jquery libjs-jquery-mousewheel libmagic1 \ sqlite3 # Install Roundcube from source if it is not already present or if it is out of date. @@ -145,6 +145,7 @@ cat > $RCM_CONFIG < EOF @@ -170,8 +171,8 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php < Date: Sat, 4 May 2024 14:27:11 -0400 Subject: [PATCH 02/55] fixing subprocess import --- management/mailconfig.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/management/mailconfig.py b/management/mailconfig.py index f9c8f237..1cd4ec40 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -10,6 +10,8 @@ # address entered by the user. import os, sqlite3, re +import subprocess + import utils from email_validator import validate_email as validate_email_, EmailNotValidError import idna From 173501e8b0ac0f3479f2a6712c0cf1872df33542 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 21:01:09 -0400 Subject: [PATCH 03/55] fixing imap sed script --- setup/mail-dovecot.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 1a9fb1fc..baab4b76 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -67,9 +67,9 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ -sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf -if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then - sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf +sed -i "s/#mail_plugins =(.*)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then + sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi # configure stuff for quota support From 51c483117dac1b55dc253ff64d7319dff3babd25 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 21:15:05 -0400 Subject: [PATCH 04/55] fixing parens --- setup/mail-dovecot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index baab4b76..0cfeafc4 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -67,7 +67,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ -sed -i "s/#mail_plugins =(.*)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi From 0cd8e4db62b237c170f4688b7d07ef2f0cbd03a5 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 22:22:47 -0400 Subject: [PATCH 05/55] fixing imap sed script --- setup/mail-dovecot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 0cfeafc4..99989d32 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -69,7 +69,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then - sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf + sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi # configure stuff for quota support From 8a1e803f48a1e824bf785337eb816a8e3bf6a503 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:14:27 -0400 Subject: [PATCH 06/55] removing duplicate conf --- conf/dovecot/conf.d/15-mailboxes.conf | 63 --------------------------- setup/mail-dovecot.sh | 2 +- 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 conf/dovecot/conf.d/15-mailboxes.conf diff --git a/conf/dovecot/conf.d/15-mailboxes.conf b/conf/dovecot/conf.d/15-mailboxes.conf deleted file mode 100644 index 58e2efed..00000000 --- a/conf/dovecot/conf.d/15-mailboxes.conf +++ /dev/null @@ -1,63 +0,0 @@ -## NOTE: This file is automatically generated by Mail-in-a-Box. -## Do not edit this file. It is continually updated by -## Mail-in-a-Box and your changes will be lost. -## -## Mail-in-a-Box machines are not meant to be modified. -## If you modify any system configuration you are on -## your own --- please do not ask for help from us. - -namespace inbox { - # Automatically create & subscribe some folders. - # * Create and subscribe the INBOX folder. - # * Our sieve rule for spam expects that the Spam folder exists. - # * Z-Push must be configured with the same settings in conf/zpush/backend_imap.php (#580). - - # MUA notes: - # * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359). - # * K-9 mail will poll every 90 seconds if a Drafts folder does not exist. - # * Apple's OS X Mail app will create 'Sent Messages' if it doesn't see a folder with the \Sent flag (#571, #573) and won't be able to archive messages unless 'Archive' exists (#581). - # * Thunderbird's default in its UI is 'Archives' (plural) but it will configure new accounts to use whatever we say here (#581). - - # auto: - # 'create' will automatically create this mailbox. - # 'subscribe' will both create and subscribe to the mailbox. - - # special_use is a space separated list of IMAP SPECIAL-USE - # attributes as specified by RFC 6154: - # \All \Archive \Drafts \Flagged \Junk \Sent \Trash - - mailbox INBOX { - auto = subscribe - } - mailbox Spam { - special_use = \Junk - auto = subscribe - } - mailbox Drafts { - special_use = \Drafts - auto = subscribe - } - mailbox Sent { - special_use = \Sent - auto = subscribe - } - mailbox Trash { - special_use = \Trash - auto = subscribe - } - mailbox Archive { - special_use = \Archive - auto = subscribe - } - - # dovevot's standard mailboxes configuration file marks two sent folders - # with the \Sent attribute, just in case clients don't agree about which - # they're using. We'll keep that, plus add Junk as an alterative for Spam. - # These are not auto-created. - mailbox "Sent Messages" { - special_use = \Sent - } - mailbox Junk { - special_use = \Junk - } -} diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 99989d32..5e8eb93d 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -66,7 +66,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ first_valid_uid=0 # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. -cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ +cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf From 1c66f69fd9f5f874de540523be54077863d9be43 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:25:46 -0400 Subject: [PATCH 07/55] using migrations for alter table command --- setup/mail-users.sh | 2 -- setup/migrate.py | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 99dcea37..58576585 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -24,8 +24,6 @@ if [ ! -f "$db_path" ]; then echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 "$db_path"; echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; -elif sqlite3 $db_path ".schema users" | grep --invert-match quota; then - echo "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';" | sqlite3 $db_path; fi # ### User Authentication diff --git a/setup/migrate.py b/setup/migrate.py index 066e0e03..9f0ddc97 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -190,6 +190,13 @@ def migration_14(env): db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"]) +def migration_15(env): + # Add a column to the users table to store their quota limit. Default to '0' for unlimited. + db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') + shell("check_call", ["sqlite3", db, + "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';"]) + + ########################################################### def get_current_migration(): From 4e9c564c5df527f2c287da8bc22c27d6fde4268d Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:37:42 -0400 Subject: [PATCH 08/55] fixing cli commands --- management/cli.py | 11 +++++------ setup/migrate.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/management/cli.py b/management/cli.py index 1c3fa4bd..4f7a273c 100755 --- a/management/cli.py +++ b/management/cli.py @@ -9,13 +9,13 @@ import sys, getpass, urllib.request, urllib.error, json, csv import contextlib -def mgmt(cmd, data=None, is_json=False): +def mgmt(cmd, data=None, is_json=False, method='GET'): # The base URL for the management daemon. (Listens on IPv4 only.) mgmt_uri = 'http://127.0.0.1:10222' setup_key_auth(mgmt_uri) - req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None) + req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None, method=method) try: response = urllib.request.urlopen(req) except urllib.error.HTTPError as e: @@ -66,7 +66,7 @@ if len(sys.argv) < 2: {cli} user password user@domain.com [password] {cli} user remove user@domain.com {cli} user make-admin user@domain.com - {cli} user quota user@domain [new-quota] + {cli} user quota user@domain [new-quota] (get or set user quota) {cli} user remove-admin user@domain.com {cli} user admins (lists admins) {cli} user mfa show user@domain.com (shows MFA devices for user, if any) @@ -124,12 +124,12 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins": print(user['email']) elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4: - # Set a user's quota + # Get a user's quota print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3])) elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5: # Set a user's quota - users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }) + users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }, method='POST') elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]: # Show MFA status for a user. @@ -161,4 +161,3 @@ elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv else: print("Invalid command-line arguments.") sys.exit(1) - diff --git a/setup/migrate.py b/setup/migrate.py index 9f0ddc97..ec5fdf9c 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -276,4 +276,3 @@ if __name__ == "__main__": elif sys.argv[-1] == "--migrate": # Perform migrations. run_migrations() - From 7c7b744605ed3a78c1c3f956601033bb547948d3 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Fri, 12 Jul 2024 13:54:49 -0400 Subject: [PATCH 09/55] removing the ability to configure the default quota -- default quota is always unlimited. --- management/cli.py | 7 ------- management/daemon.py | 27 ++------------------------- management/mailconfig.py | 6 +----- management/templates/users.html | 11 +---------- setup/migrate.py | 3 +-- 5 files changed, 5 insertions(+), 49 deletions(-) diff --git a/management/cli.py b/management/cli.py index 4f7a273c..b45e912f 100755 --- a/management/cli.py +++ b/management/cli.py @@ -60,7 +60,6 @@ def setup_key_auth(mgmt_uri): if len(sys.argv) < 2: print("""Usage: - {cli} system default-quota [new default] (set default quota for system) {cli} user (lists users) {cli} user add user@domain.com [password] {cli} user password user@domain.com [password] @@ -152,12 +151,6 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5: elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4: print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] })) -elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 3: - print(mgmt("/system/default-quota?text=1")) - -elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 4: - print(mgmt("/system/default-quota", { "default_quota": sys.argv[3]})) - else: print("Invalid command-line arguments.") sys.exit(1) diff --git a/management/daemon.py b/management/daemon.py index e850e6c6..2ad8c480 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -21,7 +21,7 @@ import auth, utils from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user 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_quota, set_mail_quota, get_default_quota, validate_quota +from mailconfig import get_mail_quota, set_mail_quota from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa import contextlib @@ -192,7 +192,7 @@ def mail_users(): @app.route('/mail/users/add', methods=['POST']) @authorized_personnel_only def mail_users_add(): - quota = request.form.get('quota', get_default_quota(env)) + quota = request.form.get('quota', '0') try: return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, env) except ValueError as e: @@ -675,29 +675,6 @@ def privacy_status_set(): utils.write_settings(config, env) return "OK" -@app.route('/system/default-quota', methods=["GET"]) -@authorized_personnel_only -def default_quota_get(): - if request.values.get('text'): - return get_default_quota(env) - else: - return json_response({ - "default-quota": get_default_quota(env), - }) - -@app.route('/system/default-quota', methods=["POST"]) -@authorized_personnel_only -def default_quota_set(): - config = utils.load_settings(env) - try: - config["default-quota"] = validate_quota(request.values.get('default_quota')) - utils.write_settings(config, env) - - except ValueError as e: - return ("ERROR: %s" % str(e), 400) - - return "OK" - # MUNIN @app.route('/munin/') diff --git a/management/mailconfig.py b/management/mailconfig.py index 1cd4ec40..d61bb8f1 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -344,7 +344,7 @@ def add_mail_user(email, pw, privs, quota, env): if validation: return validation if quota is None: - quota = get_default_quota() + quota = '0' try: quota = validate_quota(quota) @@ -429,10 +429,6 @@ def dovecot_quota_recalc(email): # force dovecot to recalculate the quota info for the user. subprocess.call(["doveadm", "quota", "recalc", "-u", email]) -def get_default_quota(env): - config = utils.load_settings(env) - return config.get("default-quota", '0') - def validate_quota(quota): # validate quota quota = quota.strip().upper() diff --git a/management/templates/users.html b/management/templates/users.html index da04fcc9..e46b8391 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -30,7 +30,7 @@
- +
@@ -165,15 +165,6 @@ curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/us diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 00d1b214..6f85bec9 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -89,4 +89,3 @@ fi # Start setup script. setup/start.sh - diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index b146e44a..1a9fb1fc 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -66,7 +66,33 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ first_valid_uid=0 # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. -cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf +cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ +sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then + sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf +fi + +# configure stuff for quota support +if ! grep -q "quota_status_success = DUNNO" /etc/dovecot/conf.d/90-quota.conf; then + cat > /etc/dovecot/conf.d/90-quota.conf << EOF; +plugin { + quota = maildir + + quota_grace = 10% + + quota_status_success = DUNNO + quota_status_nouser = DUNNO + quota_status_overquota = "522 5.2.2 Mailbox is full" +} + +service quota-status { + executable = quota-status -p postfix + inet_listener { + port = 12340 + } +} +EOF +fi # ### IMAP/POP diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 7b642a2a..5a4c7fec 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -238,7 +238,7 @@ tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit= # "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC tools/editconf.py /etc/postfix/main.cf \ smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org=127.0.1.[2..99]" \ - smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023" + smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023,check_policy_service inet:127.0.0.1:12340" # Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that # Postgrey listens on the same interface (and not IPv6, for instance). diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 25a21c41..9b943cef 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -20,7 +20,7 @@ db_path=$STORAGE_ROOT/mail/users.sqlite # Create an empty database if it doesn't yet exist. if [ ! -f "$db_path" ]; then echo "Creating new user database: $db_path"; - echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 "$db_path"; + echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', quota TEXT NOT NULL DEFAULT '0');" | sqlite3 "$db_path"; echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 "$db_path"; echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; @@ -51,7 +51,7 @@ driver = sqlite connect = $db_path default_pass_scheme = SHA512-CRYPT password_query = SELECT email as user, password FROM users WHERE email='%u'; -user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users WHERE email='%u'; +user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home, '*:bytes=' || quota AS quota_rule FROM users WHERE email='%u'; iterate_query = SELECT email AS user FROM users; EOF chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions @@ -159,4 +159,5 @@ EOF restart_service postfix restart_service dovecot - +# force a recalculation of all user quotas +doveadm quota recalc -A diff --git a/setup/webmail.sh b/setup/webmail.sh index 4591119c..3fc4f230 100644 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -145,6 +145,7 @@ cat > $RCM_CONFIG < EOF From 1cbf06c42a3c472d3b555e62ee037ee93296fe6f Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 14:27:11 -0400 Subject: [PATCH 23/55] fixing subprocess import --- management/mailconfig.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/management/mailconfig.py b/management/mailconfig.py index 6d53885b..ae366310 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -10,6 +10,8 @@ # address entered by the user. import os, sqlite3, re +import subprocess + import utils from email_validator import validate_email as validate_email_, EmailNotValidError import idna From bf96bb93f3c30cc724081937ffd59fd0ee72ffe7 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 21:01:09 -0400 Subject: [PATCH 24/55] fixing imap sed script --- setup/mail-dovecot.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 1a9fb1fc..baab4b76 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -67,9 +67,9 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ -sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf -if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then - sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf +sed -i "s/#mail_plugins =(.*)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then + sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi # configure stuff for quota support From 8bc59caffccc76f2cafd37f69d1373fde98a3448 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 21:15:05 -0400 Subject: [PATCH 25/55] fixing parens --- setup/mail-dovecot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index baab4b76..0cfeafc4 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -67,7 +67,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ -sed -i "s/#mail_plugins =(.*)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi From 7b57861ac9d53ddc45fc8cb5e8c4e9a7bd02c780 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 22:22:47 -0400 Subject: [PATCH 26/55] fixing imap sed script --- setup/mail-dovecot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 0cfeafc4..99989d32 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -69,7 +69,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then - sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf + sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi # configure stuff for quota support From 1f37e16db420bcf1e6d2d0a1a1577a6083384fe1 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:14:27 -0400 Subject: [PATCH 27/55] removing duplicate conf --- conf/dovecot/conf.d/15-mailboxes.conf | 63 --------------------------- setup/mail-dovecot.sh | 2 +- 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 conf/dovecot/conf.d/15-mailboxes.conf diff --git a/conf/dovecot/conf.d/15-mailboxes.conf b/conf/dovecot/conf.d/15-mailboxes.conf deleted file mode 100644 index 58e2efed..00000000 --- a/conf/dovecot/conf.d/15-mailboxes.conf +++ /dev/null @@ -1,63 +0,0 @@ -## NOTE: This file is automatically generated by Mail-in-a-Box. -## Do not edit this file. It is continually updated by -## Mail-in-a-Box and your changes will be lost. -## -## Mail-in-a-Box machines are not meant to be modified. -## If you modify any system configuration you are on -## your own --- please do not ask for help from us. - -namespace inbox { - # Automatically create & subscribe some folders. - # * Create and subscribe the INBOX folder. - # * Our sieve rule for spam expects that the Spam folder exists. - # * Z-Push must be configured with the same settings in conf/zpush/backend_imap.php (#580). - - # MUA notes: - # * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359). - # * K-9 mail will poll every 90 seconds if a Drafts folder does not exist. - # * Apple's OS X Mail app will create 'Sent Messages' if it doesn't see a folder with the \Sent flag (#571, #573) and won't be able to archive messages unless 'Archive' exists (#581). - # * Thunderbird's default in its UI is 'Archives' (plural) but it will configure new accounts to use whatever we say here (#581). - - # auto: - # 'create' will automatically create this mailbox. - # 'subscribe' will both create and subscribe to the mailbox. - - # special_use is a space separated list of IMAP SPECIAL-USE - # attributes as specified by RFC 6154: - # \All \Archive \Drafts \Flagged \Junk \Sent \Trash - - mailbox INBOX { - auto = subscribe - } - mailbox Spam { - special_use = \Junk - auto = subscribe - } - mailbox Drafts { - special_use = \Drafts - auto = subscribe - } - mailbox Sent { - special_use = \Sent - auto = subscribe - } - mailbox Trash { - special_use = \Trash - auto = subscribe - } - mailbox Archive { - special_use = \Archive - auto = subscribe - } - - # dovevot's standard mailboxes configuration file marks two sent folders - # with the \Sent attribute, just in case clients don't agree about which - # they're using. We'll keep that, plus add Junk as an alterative for Spam. - # These are not auto-created. - mailbox "Sent Messages" { - special_use = \Sent - } - mailbox Junk { - special_use = \Junk - } -} diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 99989d32..5e8eb93d 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -66,7 +66,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ first_valid_uid=0 # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. -cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ +cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf From 1cb67f545e1a6067645923e872bea48f92e9374e Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:25:46 -0400 Subject: [PATCH 28/55] using migrations for alter table command --- setup/migrate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup/migrate.py b/setup/migrate.py index 94bea923..3d76aabc 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -190,6 +190,13 @@ def migration_14(env): db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"]) +def migration_15(env): + # Add a column to the users table to store their quota limit. Default to '0' for unlimited. + db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') + shell("check_call", ["sqlite3", db, + "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';"]) + + ########################################################### def get_current_migration(): From c01a1d5493a090eeb4e0ce3b4dfffaf031af2cb4 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:37:42 -0400 Subject: [PATCH 29/55] fixing cli commands --- management/cli.py | 11 +++++------ setup/migrate.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/management/cli.py b/management/cli.py index 1c3fa4bd..4f7a273c 100755 --- a/management/cli.py +++ b/management/cli.py @@ -9,13 +9,13 @@ import sys, getpass, urllib.request, urllib.error, json, csv import contextlib -def mgmt(cmd, data=None, is_json=False): +def mgmt(cmd, data=None, is_json=False, method='GET'): # The base URL for the management daemon. (Listens on IPv4 only.) mgmt_uri = 'http://127.0.0.1:10222' setup_key_auth(mgmt_uri) - req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None) + req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None, method=method) try: response = urllib.request.urlopen(req) except urllib.error.HTTPError as e: @@ -66,7 +66,7 @@ if len(sys.argv) < 2: {cli} user password user@domain.com [password] {cli} user remove user@domain.com {cli} user make-admin user@domain.com - {cli} user quota user@domain [new-quota] + {cli} user quota user@domain [new-quota] (get or set user quota) {cli} user remove-admin user@domain.com {cli} user admins (lists admins) {cli} user mfa show user@domain.com (shows MFA devices for user, if any) @@ -124,12 +124,12 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins": print(user['email']) elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4: - # Set a user's quota + # Get a user's quota print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3])) elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5: # Set a user's quota - users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }) + users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }, method='POST') elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]: # Show MFA status for a user. @@ -161,4 +161,3 @@ elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv else: print("Invalid command-line arguments.") sys.exit(1) - diff --git a/setup/migrate.py b/setup/migrate.py index 3d76aabc..fc0649dc 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -276,4 +276,3 @@ if __name__ == "__main__": elif sys.argv[-1] == "--migrate": # Perform migrations. run_migrations() - From ceaf5338be09f2f779e4ff737e9849bfcff07e36 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Fri, 12 Jul 2024 13:54:49 -0400 Subject: [PATCH 30/55] removing the ability to configure the default quota -- default quota is always unlimited. --- management/cli.py | 7 ------- management/daemon.py | 27 ++------------------------- management/mailconfig.py | 6 +----- management/templates/users.html | 11 +---------- setup/migrate.py | 3 +-- 5 files changed, 5 insertions(+), 49 deletions(-) diff --git a/management/cli.py b/management/cli.py index 4f7a273c..b45e912f 100755 --- a/management/cli.py +++ b/management/cli.py @@ -60,7 +60,6 @@ def setup_key_auth(mgmt_uri): if len(sys.argv) < 2: print("""Usage: - {cli} system default-quota [new default] (set default quota for system) {cli} user (lists users) {cli} user add user@domain.com [password] {cli} user password user@domain.com [password] @@ -152,12 +151,6 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5: elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4: print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] })) -elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 3: - print(mgmt("/system/default-quota?text=1")) - -elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 4: - print(mgmt("/system/default-quota", { "default_quota": sys.argv[3]})) - else: print("Invalid command-line arguments.") sys.exit(1) diff --git a/management/daemon.py b/management/daemon.py index e850e6c6..2ad8c480 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -21,7 +21,7 @@ import auth, utils from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user 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_quota, set_mail_quota, get_default_quota, validate_quota +from mailconfig import get_mail_quota, set_mail_quota from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa import contextlib @@ -192,7 +192,7 @@ def mail_users(): @app.route('/mail/users/add', methods=['POST']) @authorized_personnel_only def mail_users_add(): - quota = request.form.get('quota', get_default_quota(env)) + quota = request.form.get('quota', '0') try: return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, env) except ValueError as e: @@ -675,29 +675,6 @@ def privacy_status_set(): utils.write_settings(config, env) return "OK" -@app.route('/system/default-quota', methods=["GET"]) -@authorized_personnel_only -def default_quota_get(): - if request.values.get('text'): - return get_default_quota(env) - else: - return json_response({ - "default-quota": get_default_quota(env), - }) - -@app.route('/system/default-quota', methods=["POST"]) -@authorized_personnel_only -def default_quota_set(): - config = utils.load_settings(env) - try: - config["default-quota"] = validate_quota(request.values.get('default_quota')) - utils.write_settings(config, env) - - except ValueError as e: - return ("ERROR: %s" % str(e), 400) - - return "OK" - # MUNIN @app.route('/munin/') diff --git a/management/mailconfig.py b/management/mailconfig.py index ae366310..2f44bf25 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -344,7 +344,7 @@ def add_mail_user(email, pw, privs, quota, env): if validation: return validation if quota is None: - quota = get_default_quota() + quota = '0' try: quota = validate_quota(quota) @@ -429,10 +429,6 @@ def dovecot_quota_recalc(email): # force dovecot to recalculate the quota info for the user. subprocess.call(["doveadm", "quota", "recalc", "-u", email]) -def get_default_quota(env): - config = utils.load_settings(env) - return config.get("default-quota", '0') - def validate_quota(quota): # validate quota quota = quota.strip().upper() diff --git a/management/templates/users.html b/management/templates/users.html index da04fcc9..e46b8391 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -30,7 +30,7 @@
- +
@@ -165,15 +165,6 @@ curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/us diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 00d1b214..6f85bec9 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -89,4 +89,3 @@ fi # Start setup script. setup/start.sh - diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index b146e44a..1a9fb1fc 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -66,7 +66,33 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ first_valid_uid=0 # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. -cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf +cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ +sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then + sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf +fi + +# configure stuff for quota support +if ! grep -q "quota_status_success = DUNNO" /etc/dovecot/conf.d/90-quota.conf; then + cat > /etc/dovecot/conf.d/90-quota.conf << EOF; +plugin { + quota = maildir + + quota_grace = 10% + + quota_status_success = DUNNO + quota_status_nouser = DUNNO + quota_status_overquota = "522 5.2.2 Mailbox is full" +} + +service quota-status { + executable = quota-status -p postfix + inet_listener { + port = 12340 + } +} +EOF +fi # ### IMAP/POP diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 7b642a2a..5a4c7fec 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -238,7 +238,7 @@ tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit= # "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC tools/editconf.py /etc/postfix/main.cf \ smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org=127.0.1.[2..99]" \ - smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023" + smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023,check_policy_service inet:127.0.0.1:12340" # Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that # Postgrey listens on the same interface (and not IPv6, for instance). diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 25a21c41..9b943cef 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -20,7 +20,7 @@ db_path=$STORAGE_ROOT/mail/users.sqlite # Create an empty database if it doesn't yet exist. if [ ! -f "$db_path" ]; then echo "Creating new user database: $db_path"; - echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 "$db_path"; + echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', quota TEXT NOT NULL DEFAULT '0');" | sqlite3 "$db_path"; echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 "$db_path"; echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path"; @@ -51,7 +51,7 @@ driver = sqlite connect = $db_path default_pass_scheme = SHA512-CRYPT password_query = SELECT email as user, password FROM users WHERE email='%u'; -user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home FROM users WHERE email='%u'; +user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home, '*:bytes=' || quota AS quota_rule FROM users WHERE email='%u'; iterate_query = SELECT email AS user FROM users; EOF chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions @@ -159,4 +159,5 @@ EOF restart_service postfix restart_service dovecot - +# force a recalculation of all user quotas +doveadm quota recalc -A diff --git a/setup/webmail.sh b/setup/webmail.sh index a203cb8c..64a27fd2 100644 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -145,6 +145,7 @@ cat > $RCM_CONFIG < EOF From d8ab444d5924798b9b09f2ebcd541ac8c8db526a Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 14:27:11 -0400 Subject: [PATCH 39/55] fixing subprocess import --- management/mailconfig.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/management/mailconfig.py b/management/mailconfig.py index 6d53885b..ae366310 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -10,6 +10,8 @@ # address entered by the user. import os, sqlite3, re +import subprocess + import utils from email_validator import validate_email as validate_email_, EmailNotValidError import idna From b4170e4095eaec7bf34740dc6e900f64b217bd1f Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 21:01:09 -0400 Subject: [PATCH 40/55] fixing imap sed script --- setup/mail-dovecot.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 1a9fb1fc..baab4b76 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -67,9 +67,9 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ -sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf -if ! grep -q "mail_plugins = \$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf; then - sed -i "s/mail_plugins = .*/mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf +sed -i "s/#mail_plugins =(.*)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then + sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi # configure stuff for quota support From 425903312109a92c21104b76baf170f7a631be1d Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 21:15:05 -0400 Subject: [PATCH 41/55] fixing parens --- setup/mail-dovecot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index baab4b76..0cfeafc4 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -67,7 +67,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ -sed -i "s/#mail_plugins =(.*)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf +sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi From 55bb35e3ef1cfbce561cf1f19276d48cbba854f2 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Sat, 4 May 2024 22:22:47 -0400 Subject: [PATCH 42/55] fixing imap sed script --- setup/mail-dovecot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 0cfeafc4..99989d32 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -69,7 +69,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then - sed -i "s/mail_plugins =\(.*\)/mail_plugins =\1 imap_quota/" /etc/dovecot/conf.d/20-imap.conf + sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf fi # configure stuff for quota support From 67c502e97bb568702835d72d27d9ec8d52e0f311 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:14:27 -0400 Subject: [PATCH 43/55] removing duplicate conf --- conf/dovecot/conf.d/15-mailboxes.conf | 63 --------------------------- setup/mail-dovecot.sh | 2 +- 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 conf/dovecot/conf.d/15-mailboxes.conf diff --git a/conf/dovecot/conf.d/15-mailboxes.conf b/conf/dovecot/conf.d/15-mailboxes.conf deleted file mode 100644 index 58e2efed..00000000 --- a/conf/dovecot/conf.d/15-mailboxes.conf +++ /dev/null @@ -1,63 +0,0 @@ -## NOTE: This file is automatically generated by Mail-in-a-Box. -## Do not edit this file. It is continually updated by -## Mail-in-a-Box and your changes will be lost. -## -## Mail-in-a-Box machines are not meant to be modified. -## If you modify any system configuration you are on -## your own --- please do not ask for help from us. - -namespace inbox { - # Automatically create & subscribe some folders. - # * Create and subscribe the INBOX folder. - # * Our sieve rule for spam expects that the Spam folder exists. - # * Z-Push must be configured with the same settings in conf/zpush/backend_imap.php (#580). - - # MUA notes: - # * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359). - # * K-9 mail will poll every 90 seconds if a Drafts folder does not exist. - # * Apple's OS X Mail app will create 'Sent Messages' if it doesn't see a folder with the \Sent flag (#571, #573) and won't be able to archive messages unless 'Archive' exists (#581). - # * Thunderbird's default in its UI is 'Archives' (plural) but it will configure new accounts to use whatever we say here (#581). - - # auto: - # 'create' will automatically create this mailbox. - # 'subscribe' will both create and subscribe to the mailbox. - - # special_use is a space separated list of IMAP SPECIAL-USE - # attributes as specified by RFC 6154: - # \All \Archive \Drafts \Flagged \Junk \Sent \Trash - - mailbox INBOX { - auto = subscribe - } - mailbox Spam { - special_use = \Junk - auto = subscribe - } - mailbox Drafts { - special_use = \Drafts - auto = subscribe - } - mailbox Sent { - special_use = \Sent - auto = subscribe - } - mailbox Trash { - special_use = \Trash - auto = subscribe - } - mailbox Archive { - special_use = \Archive - auto = subscribe - } - - # dovevot's standard mailboxes configuration file marks two sent folders - # with the \Sent attribute, just in case clients don't agree about which - # they're using. We'll keep that, plus add Junk as an alterative for Spam. - # These are not auto-created. - mailbox "Sent Messages" { - special_use = \Sent - } - mailbox Junk { - special_use = \Junk - } -} diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 99989d32..5e8eb93d 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -66,7 +66,7 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ first_valid_uid=0 # Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive. -cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/ +cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf From 27c510319ff8d30bc652d2d126f1a57aeb7ed3f4 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:25:46 -0400 Subject: [PATCH 44/55] using migrations for alter table command --- setup/migrate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup/migrate.py b/setup/migrate.py index 94bea923..3d76aabc 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -190,6 +190,13 @@ def migration_14(env): db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"]) +def migration_15(env): + # Add a column to the users table to store their quota limit. Default to '0' for unlimited. + db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') + shell("check_call", ["sqlite3", db, + "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';"]) + + ########################################################### def get_current_migration(): From 8bb68d60a585457851c89f32f98a3cce7d867e61 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Mon, 8 Jul 2024 20:37:42 -0400 Subject: [PATCH 45/55] fixing cli commands --- management/cli.py | 11 +++++------ setup/migrate.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/management/cli.py b/management/cli.py index 1c3fa4bd..4f7a273c 100755 --- a/management/cli.py +++ b/management/cli.py @@ -9,13 +9,13 @@ import sys, getpass, urllib.request, urllib.error, json, csv import contextlib -def mgmt(cmd, data=None, is_json=False): +def mgmt(cmd, data=None, is_json=False, method='GET'): # The base URL for the management daemon. (Listens on IPv4 only.) mgmt_uri = 'http://127.0.0.1:10222' setup_key_auth(mgmt_uri) - req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None) + req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None, method=method) try: response = urllib.request.urlopen(req) except urllib.error.HTTPError as e: @@ -66,7 +66,7 @@ if len(sys.argv) < 2: {cli} user password user@domain.com [password] {cli} user remove user@domain.com {cli} user make-admin user@domain.com - {cli} user quota user@domain [new-quota] + {cli} user quota user@domain [new-quota] (get or set user quota) {cli} user remove-admin user@domain.com {cli} user admins (lists admins) {cli} user mfa show user@domain.com (shows MFA devices for user, if any) @@ -124,12 +124,12 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins": print(user['email']) elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4: - # Set a user's quota + # Get a user's quota print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3])) elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5: # Set a user's quota - users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }) + users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] }, method='POST') elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]: # Show MFA status for a user. @@ -161,4 +161,3 @@ elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv else: print("Invalid command-line arguments.") sys.exit(1) - diff --git a/setup/migrate.py b/setup/migrate.py index 3d76aabc..fc0649dc 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -276,4 +276,3 @@ if __name__ == "__main__": elif sys.argv[-1] == "--migrate": # Perform migrations. run_migrations() - From 654f5614af2740353ca667ab9e5960065295f4b3 Mon Sep 17 00:00:00 2001 From: Chad Furman Date: Fri, 12 Jul 2024 13:54:49 -0400 Subject: [PATCH 46/55] removing the ability to configure the default quota -- default quota is always unlimited. --- management/cli.py | 7 ------- management/daemon.py | 27 ++------------------------- management/mailconfig.py | 6 +----- management/templates/users.html | 11 +---------- setup/migrate.py | 3 +-- 5 files changed, 5 insertions(+), 49 deletions(-) diff --git a/management/cli.py b/management/cli.py index 4f7a273c..b45e912f 100755 --- a/management/cli.py +++ b/management/cli.py @@ -60,7 +60,6 @@ def setup_key_auth(mgmt_uri): if len(sys.argv) < 2: print("""Usage: - {cli} system default-quota [new default] (set default quota for system) {cli} user (lists users) {cli} user add user@domain.com [password] {cli} user password user@domain.com [password] @@ -152,12 +151,6 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5: elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4: print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] })) -elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 3: - print(mgmt("/system/default-quota?text=1")) - -elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(sys.argv) == 4: - print(mgmt("/system/default-quota", { "default_quota": sys.argv[3]})) - else: print("Invalid command-line arguments.") sys.exit(1) diff --git a/management/daemon.py b/management/daemon.py index e850e6c6..2ad8c480 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -21,7 +21,7 @@ import auth, utils from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user 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_quota, set_mail_quota, get_default_quota, validate_quota +from mailconfig import get_mail_quota, set_mail_quota from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa import contextlib @@ -192,7 +192,7 @@ def mail_users(): @app.route('/mail/users/add', methods=['POST']) @authorized_personnel_only def mail_users_add(): - quota = request.form.get('quota', get_default_quota(env)) + quota = request.form.get('quota', '0') try: return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, env) except ValueError as e: @@ -675,29 +675,6 @@ def privacy_status_set(): utils.write_settings(config, env) return "OK" -@app.route('/system/default-quota', methods=["GET"]) -@authorized_personnel_only -def default_quota_get(): - if request.values.get('text'): - return get_default_quota(env) - else: - return json_response({ - "default-quota": get_default_quota(env), - }) - -@app.route('/system/default-quota', methods=["POST"]) -@authorized_personnel_only -def default_quota_set(): - config = utils.load_settings(env) - try: - config["default-quota"] = validate_quota(request.values.get('default_quota')) - utils.write_settings(config, env) - - except ValueError as e: - return ("ERROR: %s" % str(e), 400) - - return "OK" - # MUNIN @app.route('/munin/') diff --git a/management/mailconfig.py b/management/mailconfig.py index ae366310..2f44bf25 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -344,7 +344,7 @@ def add_mail_user(email, pw, privs, quota, env): if validation: return validation if quota is None: - quota = get_default_quota() + quota = '0' try: quota = validate_quota(quota) @@ -429,10 +429,6 @@ def dovecot_quota_recalc(email): # force dovecot to recalculate the quota info for the user. subprocess.call(["doveadm", "quota", "recalc", "-u", email]) -def get_default_quota(env): - config = utils.load_settings(env) - return config.get("default-quota", '0') - def validate_quota(quota): # validate quota quota = quota.strip().upper() diff --git a/management/templates/users.html b/management/templates/users.html index da04fcc9..e46b8391 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -30,7 +30,7 @@
- +
@@ -165,15 +165,6 @@ curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/us