-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updated Quota Support to Latest Version of MiaB and resolving code review comments #2387
base: main
Are you sure you want to change the base?
Changes from all commits
1795f8a
3970038
173501e
51c4831
0cd8e4d
0408ec3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 @@ def setup_key_auth(mgmt_uri): | |
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 @@ def setup_key_auth(mgmt_uri): | |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this comment correct? I don't understand how this sets a user's quota. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this is going to display not set a users current 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 @@ def setup_key_auth(mgmt_uri): | |
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -102,6 +104,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 +139,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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this is the number of messages. Since we don't enforce a limit for this, it's a little confusing to report. I'd suggest removing it. |
||
"status": "active", | ||
} | ||
users.append(user) | ||
|
@@ -150,6 +197,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 +317,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 +343,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 +359,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 +393,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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this file is mistakenly added. I think it duplicates a file two directories up. I didn't check if the files are different.