-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
ce45217
d8ab444
b4170e4
4259033
55bb35e
67c502e
27c5103
8bb68d6
654f561
bd5ba78
08e69ca
c9d37be
450c192
ac383ce
7f9a348
626bced
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 |
---|---|---|
|
@@ -65,6 +65,7 @@ def setup_key_auth(mgmt_uri): | |
{cli} user password [email protected] [password] | ||
{cli} user remove [email protected] | ||
{cli} user make-admin [email protected] | ||
{cli} user quota user@domain [new-quota] (get or set user quota) | ||
{cli} user remove-admin [email protected] | ||
{cli} user admins (lists admins) | ||
{cli} user mfa show [email protected] (shows MFA devices for user, if any) | ||
|
@@ -117,6 +118,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: | ||
# 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] }) | ||
|
||
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) | ||
|
@@ -141,4 +150,3 @@ def setup_key_auth(mgmt_uri): | |
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,42 @@ 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_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) | ||
|
||
try: | ||
percent = (box_size / box_quota) * 100 | ||
except: | ||
percent = 'Error' | ||
|
||
except: | ||
box_size = '?' | ||
box_quota = '?' | ||
percent = '?' | ||
|
||
if quota == '0': | ||
percent = '' | ||
|
||
user = { | ||
"email": email, | ||
"privileges": parse_privs(privileges), | ||
"quota": 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. How about adding these new user keys (quota, box_quota, box_size and percent) to the comment block at the beginning of the function? |
||
"box_quota": box_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. From reading the code I'm having a little trouble understanding the difference between quota and box_quota. |
||
"box_size": sizeof_fmt(box_size) if box_size != '?' else box_size, | ||
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. Am I correct to understand that box_size represents the current size of mail used by this user account? |
||
"percent": '%3.0f%%' % percent if type(percent) != str else percent, | ||
"status": "active", | ||
} | ||
users.append(user) | ||
|
@@ -150,6 +193,9 @@ def get_mail_users_ex(env, with_archived=False): | |
"privileges": [], | ||
"status": "inactive", | ||
"mailbox": mbox, | ||
"box_size": '?', | ||
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. There's no default quota key/value being added to the inactive users. Is that intentional or oversight? |
||
"box_quota": '?', | ||
"percent": '?', | ||
} | ||
users.append(user) | ||
|
||
|
@@ -266,7 +312,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 +338,14 @@ def add_mail_user(email, pw, privs, env): | |
validation = validate_privilege(p) | ||
if validation: return validation | ||
|
||
if quota is None: | ||
quota = '0' | ||
|
||
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 +354,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 +388,55 @@ 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]) | ||
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. The utils.py file has a function for exec'ing external processes already called "shell". Is there a reason not to use that here? |
||
|
||
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.
If a num that's too large gets passed in the function returns a bad result. Maybe consider adding input validation.