diff --git a/admin/mailu/admin/forms.py b/admin/mailu/admin/forms.py
index c17128a0..015a6df1 100644
--- a/admin/mailu/admin/forms.py
+++ b/admin/mailu/admin/forms.py
@@ -46,6 +46,7 @@ class DomainForm(flask_wtf.FlaskForm):
name = fields.StringField(_('Domain name'), [validators.DataRequired()])
max_users = fields_.IntegerField(_('Maximum user count'), default=10)
max_aliases = fields_.IntegerField(_('Maximum alias count'), default=10)
+ max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0)
comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Create'))
diff --git a/admin/mailu/admin/models.py b/admin/mailu/admin/models.py
index d7d46e65..ff46b9f9 100644
--- a/admin/mailu/admin/models.py
+++ b/admin/mailu/admin/models.py
@@ -57,6 +57,7 @@ class Domain(Base):
backref=db.backref('manager_of'), lazy='dynamic')
max_users = db.Column(db.Integer, nullable=False, default=0)
max_aliases = db.Column(db.Integer, nullable=False, default=0)
+ max_quota_bytes = db.Column(db.Integer(), nullable=False, default=0)
@property
def dkim_key(self):
diff --git a/admin/mailu/admin/templates/domain/create.html b/admin/mailu/admin/templates/domain/create.html
index 29a4f431..301d91ac 100644
--- a/admin/mailu/admin/templates/domain/create.html
+++ b/admin/mailu/admin/templates/domain/create.html
@@ -9,6 +9,9 @@
{{ form.hidden_tag() }}
{{ macros.form_field(form.name) }}
{{ macros.form_fields((form.max_users, form.max_aliases)) }}
+ {{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000,
+ prepend=''+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+' GiB',
+ oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }}
diff --git a/admin/mailu/admin/templates/user/create.html b/admin/mailu/admin/templates/user/create.html
index d0907ed1..829b6c80 100644
--- a/admin/mailu/admin/templates/user/create.html
+++ b/admin/mailu/admin/templates/user/create.html
@@ -13,7 +13,7 @@
{{ form.hidden_tag() }}
{{ macros.form_field(form.localpart, append='@'+domain.name+'') }}
{{ macros.form_fields((form.pw, form.pw2)) }}
- {{ macros.form_field(form.quota_bytes, step=1000000000, max=50000000000,
+ {{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50000000000),
prepend=''+(form.quota_bytes.data//1000000000).__str__()+' GiB',
oninput='$("#quota").text(this.value/1000000000);') }}
{{ macros.form_field(form.enable_imap) }}
diff --git a/admin/mailu/admin/views/users.py b/admin/mailu/admin/views/users.py
index 9fa7bb1b..b7647138 100644
--- a/admin/mailu/admin/views/users.py
+++ b/admin/mailu/admin/views/users.py
@@ -2,6 +2,7 @@ from mailu.admin import app, db, models, forms, access
import flask
import flask_login
+import wtforms
import wtforms_components
@@ -21,6 +22,8 @@ def user_create(domain_name):
return flask.redirect(
flask.url_for('.user_list', domain_name=domain.name))
form = forms.UserForm()
+ form.quota_bytes.validators = [
+ wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
if form.validate_on_submit():
if domain.has_email(form.localpart.data):
flask.flash('Email is already used', 'error')
@@ -41,10 +44,17 @@ def user_create(domain_name):
@access.domain_admin(models.User, 'user_email')
def user_edit(user_email):
user = models.User.query.get(user_email) or flask.abort(404)
+ # Handle the case where user quota is more than allowed
+ max_quota_bytes = user.domain.max_quota_bytes
+ if max_quota_bytes and user.quota_bytes > max_quota_bytes:
+ max_quota_bytes = user.quota_bytes
+ # Create the form
form = forms.UserForm(obj=user)
wtforms_components.read_only(form.localpart)
form.pw.validators = []
form.localpart.validators = []
+ form.quota_bytes.validators = [
+ wtforms.validators.NumberRange(max=max_quota_bytes)]
if form.validate_on_submit():
form.populate_obj(user)
if form.pw.data:
@@ -53,7 +63,8 @@ def user_edit(user_email):
flask.flash('User %s updated' % user)
return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name))
- return flask.render_template('user/edit.html', form=form, user=user, domain=user.domain)
+ return flask.render_template('user/edit.html', form=form, user=user,
+ domain=user.domain, max_quota_bytes=max_quota_bytes)
@app.route('/user/delete/', methods=['GET', 'POST'])
diff --git a/admin/migrations/versions/2335c80a6bc3_.py b/admin/migrations/versions/2335c80a6bc3_.py
new file mode 100644
index 00000000..ac5c1e83
--- /dev/null
+++ b/admin/migrations/versions/2335c80a6bc3_.py
@@ -0,0 +1,23 @@
+""" Add a maximum quota per domain
+
+Revision ID: 2335c80a6bc3
+Revises: 12e9a4f6ed73
+Create Date: 2016-12-04 12:57:37.576622
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2335c80a6bc3'
+down_revision = '12e9a4f6ed73'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('domain', sa.Column('max_quota_bytes', sa.Integer(), nullable=False, server_default='0'))
+
+
+def downgrade():
+ with op.batch_alter_table('domain') as batch:
+ batch.drop_column('max_quota_bytes')