diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py
index 901054a8..48f7b91e 100644
--- a/core/admin/mailu/models.py
+++ b/core/admin/mailu/models.py
@@ -60,6 +60,7 @@ class Domain(Base):
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)
+ signup_enabled = db.Column(db.Boolean(), nullable=False, default=False)
@property
def dkim_key(self):
diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py
index 451db811..1d27141d 100644
--- a/core/admin/mailu/ui/forms.py
+++ b/core/admin/mailu/ui/forms.py
@@ -47,6 +47,7 @@ class DomainForm(flask_wtf.FlaskForm):
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)
+ signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False)
comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Create'))
@@ -74,6 +75,13 @@ class UserForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Save'))
+class UserSignupForm(flask_wtf.FlaskForm):
+ localpart = fields.StringField(_('Email address'))
+ pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
+ pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
+ submit = fields.SubmitField(_('Sign up'))
+
+
class UserSettingsForm(flask_wtf.FlaskForm):
displayed_name = fields.StringField(_('Displayed name'))
spam_enabled = fields.BooleanField(_('Enable spam filter'))
diff --git a/core/admin/mailu/ui/templates/sidebar.html b/core/admin/mailu/ui/templates/sidebar.html
index 22bbfce5..d107c15f 100644
--- a/core/admin/mailu/ui/templates/sidebar.html
+++ b/core/admin/mailu/ui/templates/sidebar.html
@@ -100,10 +100,17 @@
{% else %}
-
+
{% trans %}Sign in{% endtrans %}
+ {% if signup_domains %}
+
+
+ {% trans %}Sign up{% endtrans %}
+
+
+ {% endif %}
{% endif %}
diff --git a/core/admin/mailu/ui/templates/user/signup.html b/core/admin/mailu/ui/templates/user/signup.html
new file mode 100644
index 00000000..eccd9f67
--- /dev/null
+++ b/core/admin/mailu/ui/templates/user/signup.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+
+{% block title %}
+{% trans %}Sign up{% endtrans %}
+{% endblock %}
+
+{% block subtitle %}
+{{ domain }}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/core/admin/mailu/ui/templates/user/signup_domain.html b/core/admin/mailu/ui/templates/user/signup_domain.html
new file mode 100644
index 00000000..d5df77dd
--- /dev/null
+++ b/core/admin/mailu/ui/templates/user/signup_domain.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+
+{% block title %}
+{% trans %}Sign up{% endtrans %}
+{% endblock %}
+
+{% block subtitle %}
+{% trans %}pick a domain for the new account{% endtrans %}
+{% endblock %}
+
+{% block content %}
+{% call macros.table() %}
+
+ {% trans %}Domain{% endtrans %} |
+ {% trans %}Available slots{% endtrans %} |
+ {% trans %}Quota{% endtrans %} |
+
+{% for domain_name, domain in available_domains.items() %}
+
+ {{ domain_name }} |
+ {{ domain.max_users - domain.users if domain.max_users else '∞' }} |
+ {{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }} |
+
+{% endfor %}
+{% endcall %}
+{% endblock %}
diff --git a/core/admin/mailu/ui/views/users.py b/core/admin/mailu/ui/views/users.py
index 34f7be8f..77499699 100644
--- a/core/admin/mailu/ui/views/users.py
+++ b/core/admin/mailu/ui/views/users.py
@@ -153,3 +153,35 @@ def user_reply(user_email):
return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name))
return flask.render_template('user/reply.html', form=form, user=user)
+
+
+@ui.route('/user/signup', methods=['GET', 'POST'])
+@ui.route('/user/signup/', methods=['GET', 'POST'])
+def user_signup(domain_name=None):
+ available_domains = {
+ domain.name: domain
+ for domain in models.Domain.query.filter_by(signup_enabled=True).all()
+ if not domain.max_users or len(domain.users) < domain.max_users
+ }
+ if not available_domains:
+ flask.flash('No domain available for registration')
+ if not domain_name:
+ return flask.render_template('user/signup_domain.html',
+ available_domains=available_domains)
+ domain = available_domains.get(domain_name) or flask.abort(404)
+ quota_bytes = min(config['DEFAULT_QUOTA'], domain.max_quota_bytes)
+ form = forms.UserSignupForm()
+ if form.validate_on_submit():
+ if domain.has_email(form.localpart.data):
+ flask.flash('Email is already used', 'error')
+ else:
+ user = models.User(domain=domain)
+ form.populate_obj(user)
+ user.set_password(form.pw.data)
+ user.quota_bytes = quota_bytes
+ db.session.add(user)
+ db.session.commit()
+ user.send_welcome()
+ flask.flash('Successfully signed up %s' % user)
+ return flask.redirect(flask.url_for('.index'))
+ return flask.render_template('user/signup.html', domain=domain, form=form)