diff --git a/.gitignore b/.gitignore index 7135c1b3..97f06682 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ tools/__pycache__/ externals/ .env .vagrant +.idea/ api/docs/api-docs.html *.code-workspace diff --git a/README.md b/README.md index 0b3d320b..acb4b0d3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,213 @@ +Mail-in-a-Box with Quotas +========================= + +This is an experimental implementation of Mail-in-a-box with quota support. + +Quotas can be set and viewed in the control panel + +To set quotas from the command line, use: + + tools/mail.py user quota + +To set the system default quota for new users, use: + + tools/mail.py system default-quota + +Mailbox size recalculation by Dovecot can be forced using the command: + + doveadm quota recalc -A + +Please report any bugs on github. + + +Installing v5x-quota +----------------------- + +To install the latest version, log into your box and execute the following commands: + + $ git clone https://github.com/jrsupplee/mailinabox.git + $ cd mailinabox + $ sudo bash setup/bootstrap.sh + +Follow the standard directions for setting up an MiaB installation. There are no special installation steps for installing this version. + +The default quota is set to `0` which means unlimited. If you want to set a different default quota, follow the directions above. + + +Upgrading v5x to v5x-quota +-------------------------------- + +This is experimental software. You have been warned. + +* Rename your `mailinabox` directory to something like `miab.old` + +* Clone this repository using: + + `git clone https://github.com/jrsupplee/mailinabox.git` + +* cd into `mailinabox` and run `sudo bash setup/bootstrap.sh` On occasion there are lock errors when updating `Munin`. Just re-run `sudo setup/start.sh` until the error does not occur. + +* Note: all existing users at the time of the upgrade will have there quota set to `0` (unlimited). + + +Upgrading MiaB with quotas to a New Version +------------------------------------------- + +* `cd` into the `mailinabox` directory. + +* Execute `git pull` to download the latest changes. + +* Execute `sudo bash setup/bootstrap.sh` to checkout the latest version and re-run setup. + + +Issues +------ + +* When a user's quota is changed, any IMAP session running for that user will not recognize the new quota. To solve this a `dovecot reload` could be issued causing all current IMAP sessions to be terminated. On a system with many users, it might not be desirable to reset all users sessions to fix the quota for one user. Also if the administrator is setting the quota for several users it would result in the continual reset of those connections. + +* API docs do not include the quota endpoints. Quota API endpoints need to be added to `api/mainlinabox.yml`. + + +Changes +------- + +### v57a-quota-0.22-beta + +* Update to v57a of Mail-in-a-Box + +### v56-quota + +* Update to v56 of Mail-in-a-Box + +### v0.55-quota-0.22-beta + +* Update to v55 of Mail-in-a-Box + +### v0.53-quota-0.22-beta + +* Update to v0.53 of Mail-in-a-Box + +### v0.52-quota-0.22-beta + +* Update to v0.52 of Mail-in-a-Box + +### v0.51-quota-0.22-beta + +* Update to v0.51 of Mail-in-a-Box + +### v0.50-quota-0.22-beta + +* Update to v0.50 of Mail-in-a-Box + +### v0.48-quota-0.22-beta + +* Update to v0.48 of Mail-in-a-Box + +### v0.46-quota-0.22-beta + +* Update to v0.46 of Mail-in-a-Box + +### v0.45-quota-0.22-beta + +* Update to v0.45 of Mail-in-a-Box + +### v0.44-quota-0.22-beta + +* Update to v0.44 of Mail-in-a-Box + +### v0.43-quota-0.22-beta + +* Fix bug that crashed user list when there is an archived user. + +### v0.43-quota-0.21-beta + +* Remove extra features from the master branch + +### v0.43-quota-0.20-beta + +* Hide *set quota* for a mailbox that has been archived + +### v0.43-quota-0.19-beta + +* Add user quota API documentation to the mail users page + +### v0.43-quota-0.18-beta + +* Update to v0.43 of Mail-in-a-Box + +### v0.42b-quota-0.18-beta + +* Update to v0.42b of Mail-in-a-Box + +### v0.41-quota-0.18-beta + +* Bump version to add a new annotated tag. The last version had a plain tag which is not seen when checking for the latest version. + +### v0.41-quota-0.17-beta + +* Change status of project to beta. No changes to the code + +### v0.41-quota-0.17-alpha + +* Update the README + +### v0.41-quota-0.16-alpha + +* Update to v0.41 of Mail-in-a-Box + +### v0.40-quota-0.16-alpha + +* Fix problem with quota field on control panel that prevented adding users. + +### v0.40-quota-0.15-alpha + +* Fix bug where quotas are not recalculated when a user's quota is changed in control panel + +### v0.40-quota-0.14-alpha + +* When updating a user's quota, execute `doveadm quota recalc -u ` to forces an immediate recalculation of the user's quota. + +* Add a thousands separator (,) to the messages count in the control panel user list. + +* Execute `doveadm quota recalc -A` to force a recalculation of all user quotas when running `start.sh`. + +* Get rid of the error message complaining that the `quota` column already exists when upgrading from a previous version of `v0.40-quota`. + +### v0.40-quota-0.13-alpha + +* Add a `default-quota` setting in `settings.yaml`. + +* Add input for setting quota when entering a new user in control panel. + +* Modify `tools/mail.py` to allow for setting and getting the default system quota. + +* Modify `tools/mail.py` to allow for getting a user's quota setting. + +* Modify the mail users list in control panel to display percentage of quota used. + +### v0.40-quota-0.12-alpha + +* Update README + +### v0.40-quota-0.11-alpha + +* Read latest version from this repository not the Mail-in-a-Box master repository + +### v0.40-quota-0.1-alpha + +* First experimental release of Mail-in-a-Box for quotas. +* Quotas are working and there is basic support in the control panel and `tools/mail.py`. + + +Reference Documents +------------------- + +* https://blog.sys4.de/postfix-dovecot-mailbox-quota-en.html +* https://linuxize.com/post/install-and-configure-postfix-and-dovecot/ + + +\[BEGIN Official README] + Mail-in-a-Box ============= @@ -79,7 +289,7 @@ This is a challenge faced by everyone who runs their own mail server, with or wi Contributing and Development ---------------------------- -Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome. See [CONTRIBUTING](CONTRIBUTING.md) to get started. +Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome. See [CONTRIBUTING](CONTRIBUTING.md) to get started. The Acknowledgements diff --git a/conf/dovecot-mailboxes.conf b/conf/dovecot/conf.d/15-mailboxes.conf similarity index 100% rename from conf/dovecot-mailboxes.conf rename to conf/dovecot/conf.d/15-mailboxes.conf 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..9a02a1f5 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) @@ -923,7 +923,7 @@ def check_miab_version(env, output): latest_ver = get_latest_miab_version() if this_ver == latest_ver: - output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver) + output.print_ok("Mail-in-a-Box with quota support is up to date. You are running version %s." % this_ver) elif latest_ver is None: output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s." % this_ver) else: 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 @@ +
+ + +
    @@ -34,13 +39,18 @@
  • Use aliases to create email addresses that forward to existing accounts.
  • Administrators get access to this control panel.
  • User accounts cannot contain any international (non-ASCII) characters, but aliases can.
  • +
  • Quotas may not contain any spaces, commas or decimal points. Suffixes of G (gigabytes) and M (megabytes) are allowed. For unlimited storage enter 0 (zero)

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 58c9243e..be47b18c 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..2efa29b9 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -66,7 +66,29 @@ 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 imap-quota/" /etc/dovecot/conf.d/20-imap.conf +sed -i "s/protocol imap/mail_plugins = \$mail_plugins quota\nprotocol imap/" /etc/dovecot/conf.d/20-imap.conf + +# configure stuff for quota support +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 # ### IMAP/POP diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 0c6be0c8..7ebd1426 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -20,10 +20,12 @@ 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"; +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 index 66a85596..7cefe02b 100644 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -141,10 +141,11 @@ cat > $RCM_CONFIG < EOF