diff --git a/admin/mailu/admin/__init__.py b/admin/mailu/admin/__init__.py index fc909cf0..77df2052 100644 --- a/admin/mailu/admin/__init__.py +++ b/admin/mailu/admin/__init__.py @@ -28,4 +28,5 @@ from mailu.admin.views import \ aliases, \ users, \ domains, \ + alternatives, \ fetches diff --git a/admin/mailu/admin/forms.py b/admin/mailu/admin/forms.py index ec3c7164..41b924bf 100644 --- a/admin/mailu/admin/forms.py +++ b/admin/mailu/admin/forms.py @@ -51,6 +51,11 @@ class DomainForm(flask_wtf.FlaskForm): submit = fields.SubmitField(_('Create')) +class AlternativeForm(flask_wtf.FlaskForm): + name = fields.StringField(_('Alternative name'), [validators.DataRequired()]) + submit = fields.SubmitField(_('Create')) + + class UserForm(flask_wtf.FlaskForm): localpart = fields.StringField(_('E-mail'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) diff --git a/admin/mailu/admin/models.py b/admin/mailu/admin/models.py index a1828a84..80a81ce9 100644 --- a/admin/mailu/admin/models.py +++ b/admin/mailu/admin/models.py @@ -100,6 +100,22 @@ class Domain(Base): return False +class Alternative(Base): + """ Alternative name for a served domain. + The name "domain alias" was avoided to prevent some confusion. + """ + + __tablename__ = "alternative" + + name = db.Column(db.String(80), primary_key=True, nullable=False) + domain_name = db.Column(db.String(80), db.ForeignKey(Domain.name)) + domain = db.relationship(Domain, + backref=db.backref('alternatives', cascade='all, delete-orphan')) + + def __str__(self): + return self.name + + class Email(object): """ Abstraction for an email address (localpart and domain). """ diff --git a/admin/mailu/admin/templates/alternative/create.html b/admin/mailu/admin/templates/alternative/create.html new file mode 100644 index 00000000..75461c67 --- /dev/null +++ b/admin/mailu/admin/templates/alternative/create.html @@ -0,0 +1,9 @@ +{% extends "form.html" %} + +{% block title %} +{% trans %}Create alternative domain{% endtrans %} +{% endblock %} + +{% block subtitle %} +{{ domain }} +{% endblock %} diff --git a/admin/mailu/admin/templates/alternative/list.html b/admin/mailu/admin/templates/alternative/list.html new file mode 100644 index 00000000..547d2ecb --- /dev/null +++ b/admin/mailu/admin/templates/alternative/list.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block title %} +{% trans %}Alternative domain list{% endtrans %} +{% endblock %} + +{% block subtitle %} +{{ domain.name }} +{% endblock %} + +{% block main_action %} +{% trans %}Add alternative{% endtrans %} +{% endblock %} + +{% block box %} + + + + + + + + {% for alternative in domain.alternatives %} + + + + + + {% endfor %} + +
{% trans %}Actions{% endtrans %}{% trans %}Name{% endtrans %}{% trans %}Created{% endtrans %}
+ + {{ alternative }}{{ alternative.created_at }}
+{% endblock %} diff --git a/admin/mailu/admin/templates/domain/list.html b/admin/mailu/admin/templates/domain/list.html index ea81e4c3..46a5d723 100644 --- a/admin/mailu/admin/templates/domain/list.html +++ b/admin/mailu/admin/templates/domain/list.html @@ -36,6 +36,9 @@       + {% if current_user.global_admin %} +   + {% endif %} {{ domain.name }} {{ domain.users | count }} / {{ domain.max_users or '∞' }} diff --git a/admin/mailu/admin/views/alternatives.py b/admin/mailu/admin/views/alternatives.py new file mode 100644 index 00000000..b3b4ab3b --- /dev/null +++ b/admin/mailu/admin/views/alternatives.py @@ -0,0 +1,46 @@ +from mailu.admin import app, db, models, forms, access + +import flask +import wtforms_components + + +@app.route('/alternative/list/', methods=['GET']) +@access.global_admin +def alternative_list(domain_name): + domain = models.Domain.query.get(domain_name) or flask.abort(404) + return flask.render_template('alternative/list.html', domain=domain) + + +@app.route('/alternative/create/', methods=['GET', 'POST']) +@access.global_admin +def alternative_create(domain_name): + domain = models.Domain.query.get(domain_name) or flask.abort(404) + form = forms.AlternativeForm() + if form.validate_on_submit(): + conflicting_domain = models.Domain.query.get(form.name.data) + conflicting_alternative = models.Alternative.query.get(form.name.data) + if conflicting_domain or conflicting_alternative: + flask.flash('Domain %s is already used' % form.name.data, 'error') + else: + alternative = models.Alternative(domain=domain) + form.populate_obj(alternative) + db.session.add(alternative) + db.session.commit() + flask.flash('Alternative domain %s created' % alternative) + return flask.redirect( + flask.url_for('.alternative_list', domain_name=domain.name)) + return flask.render_template('alternative/create.html', + domain=domain, form=form) + + +@app.route('/alternative/delete/', methods=['GET', 'POST']) +@access.global_admin +@access.confirmation_required("delete {alternative}") +def alternative_delete(alternative): + alternative = models.Alternative.query.get(alternative) or flask.abort(404) + domain = alternative.domain + db.session.delete(alternative) + db.session.commit() + flask.flash('Alternative %s deleted' % alternative) + return flask.redirect( + flask.url_for('.alternative_list', domain_name=domain.name)) diff --git a/admin/mailu/admin/views/domains.py b/admin/mailu/admin/views/domains.py index 9bd26641..371b46f9 100644 --- a/admin/mailu/admin/views/domains.py +++ b/admin/mailu/admin/views/domains.py @@ -16,7 +16,9 @@ def domain_list(): def domain_create(): form = forms.DomainForm() if form.validate_on_submit(): - if models.Domain.query.get(form.name.data): + conflicting_domain = models.Domain.query.get(form.name.data) + conflicting_alternative = models.Alternative.query.get(form.name.data) + if conflicting_domain or conflicting_alternative: flask.flash('Domain %s is already used' % form.name.data, 'error') else: domain = models.Domain() diff --git a/admin/migrations/versions/c9a0b4e653cf_.py b/admin/migrations/versions/c9a0b4e653cf_.py new file mode 100644 index 00000000..8882d079 --- /dev/null +++ b/admin/migrations/versions/c9a0b4e653cf_.py @@ -0,0 +1,30 @@ +""" Add alternative domains + +Revision ID: c9a0b4e653cf +Revises: 73e56bad5ec5 +Create Date: 2017-09-03 18:23:36.356527 + +""" + +# revision identifiers, used by Alembic. +revision = 'c9a0b4e653cf' +down_revision = '73e56bad5ec5' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('alternative', + sa.Column('created_at', sa.Date(), nullable=False), + sa.Column('updated_at', sa.Date(), nullable=True), + sa.Column('comment', sa.String(length=255), nullable=True), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('domain_name', sa.String(length=80), nullable=True), + sa.ForeignKeyConstraint(['domain_name'], ['domain.name'], ), + sa.PrimaryKeyConstraint('name') + ) + + +def downgrade(): + op.drop_table('alternative') diff --git a/postfix/conf/sqlite-virtual_alias_maps.cf b/postfix/conf/sqlite-virtual_alias_maps.cf index a96fb034..f53b65ae 100644 --- a/postfix/conf/sqlite-virtual_alias_maps.cf +++ b/postfix/conf/sqlite-virtual_alias_maps.cf @@ -4,7 +4,9 @@ query = FROM (SELECT destination, email, wildcard, localpart FROM alias UNION - SELECT (CASE WHEN forward_enabled=1 THEN (CASE WHEN forward_keep=1 THEN email||',' ELSE '' END)||forward_destination ELSE email END) AS destination, email, 0 as wildcard, localpart FROM user) + SELECT (CASE WHEN forward_enabled=1 THEN (CASE WHEN forward_keep=1 THEN email||',' ELSE '' END)||forward_destination ELSE email END) AS destination, email, 0 as wildcard, localpart FROM user + UNION + SELECT '@'||domain_name as destination, '@'||name as email, 0 as wildcard, '' as localpart FROM alternative) WHERE ( wildcard = 0 diff --git a/postfix/conf/sqlite-virtual_mailbox_domains.cf b/postfix/conf/sqlite-virtual_mailbox_domains.cf index 2095ef2a..af453bce 100644 --- a/postfix/conf/sqlite-virtual_mailbox_domains.cf +++ b/postfix/conf/sqlite-virtual_mailbox_domains.cf @@ -1,2 +1,5 @@ dbpath = /data/main.db -query = SELECT name FROM domain WHERE name='%s' +query = + SELECT name FROM domain WHERE name='%s' + UNION + SELECT name FROM alternative WHERE name='%s'