diff --git a/admin/mailu/admin/__init__.py b/admin/mailu/admin/__init__.py index 77df2052..fc895134 100644 --- a/admin/mailu/admin/__init__.py +++ b/admin/mailu/admin/__init__.py @@ -28,5 +28,6 @@ from mailu.admin.views import \ aliases, \ users, \ domains, \ + relays, \ alternatives, \ fetches diff --git a/admin/mailu/admin/forms.py b/admin/mailu/admin/forms.py index 41b924bf..b00f4dac 100644 --- a/admin/mailu/admin/forms.py +++ b/admin/mailu/admin/forms.py @@ -56,6 +56,13 @@ class AlternativeForm(flask_wtf.FlaskForm): submit = fields.SubmitField(_('Create')) +class RelayForm(flask_wtf.FlaskForm): + name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()]) + smtp = fields.StringField(_('Remote host')) + comment = fields.StringField(_('Comment')) + 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 80a81ce9..9fcfca1d 100644 --- a/admin/mailu/admin/models.py +++ b/admin/mailu/admin/models.py @@ -116,6 +116,20 @@ class Alternative(Base): return self.name +class Relay(Base): + """ Relayed mail domain. + The domain is either relayed publicly or through a specified SMTP host. + """ + + __tablename__ = "relay" + + name = db.Column(db.String(80), primary_key=True, nullable=False) + smtp = db.Column(db.String(80), nullable=True) + + def __str__(self): + return self.name + + class Email(object): """ Abstraction for an email address (localpart and domain). """ diff --git a/admin/mailu/admin/templates/relay/create.html b/admin/mailu/admin/templates/relay/create.html new file mode 100644 index 00000000..b22cb001 --- /dev/null +++ b/admin/mailu/admin/templates/relay/create.html @@ -0,0 +1,5 @@ +{% extends "form.html" %} + +{% block title %} +{% trans %}New relay domain{% endtrans %} +{% endblock %} diff --git a/admin/mailu/admin/templates/relay/edit.html b/admin/mailu/admin/templates/relay/edit.html new file mode 100644 index 00000000..83dafcba --- /dev/null +++ b/admin/mailu/admin/templates/relay/edit.html @@ -0,0 +1,9 @@ +{% extends "form.html" %} + +{% block title %} +{% trans %}Edit relayd domain{% endtrans %} +{% endblock %} + +{% block subtitle %} +{{ relay }} +{% endblock %} diff --git a/admin/mailu/admin/templates/relay/list.html b/admin/mailu/admin/templates/relay/list.html new file mode 100644 index 00000000..ba72a227 --- /dev/null +++ b/admin/mailu/admin/templates/relay/list.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %} +{% trans %}Relayed domain list{% endtrans %} +{% endblock %} + +{% block main_action %} +{% if current_user.global_admin %} +{% trans %}New relayed domain{% endtrans %} +{% endif %} +{% endblock %} + +{% block box %} + + + + + + + + + + + {% for relay in relays %} + + + + + + + + + {% endfor %} + +
{% trans %}Actions{% endtrans %}{% trans %}Domain name{% endtrans %}{% trans %}Remote host{% endtrans %}{% trans %}Comment{% endtrans %}{% trans %}Created{% endtrans %}{% trans %}Last edit{% endtrans %}
+   +   + {{ relay.name }}{{ relay.smtp or '-' }}{{ relay.comment or '' }}{{ relay.created_at }}{{ relay.updated_at or '' }}
+{% endblock %} diff --git a/admin/mailu/admin/templates/sidebar.html b/admin/mailu/admin/templates/sidebar.html index bd7c0439..14abe241 100644 --- a/admin/mailu/admin/templates/sidebar.html +++ b/admin/mailu/admin/templates/sidebar.html @@ -50,6 +50,11 @@ {% trans %}Administrators{% endtrans %} +
  • + + {% trans %}Relayed domains{% endtrans %} + +
  • {% endif %} {% if current_user.manager_of or current_user.global_admin %}
  • diff --git a/admin/mailu/admin/views/alternatives.py b/admin/mailu/admin/views/alternatives.py index b3b4ab3b..ce237d45 100644 --- a/admin/mailu/admin/views/alternatives.py +++ b/admin/mailu/admin/views/alternatives.py @@ -19,7 +19,8 @@ def alternative_create(domain_name): 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: + conflicting_relay = models.Relay.query.get(form.name.data) + if conflicting_domain or conflicting_alternative or conflicting_relay: flask.flash('Domain %s is already used' % form.name.data, 'error') else: alternative = models.Alternative(domain=domain) diff --git a/admin/mailu/admin/views/domains.py b/admin/mailu/admin/views/domains.py index 371b46f9..cebe5c8b 100644 --- a/admin/mailu/admin/views/domains.py +++ b/admin/mailu/admin/views/domains.py @@ -18,7 +18,8 @@ def domain_create(): 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: + conflicting_relay = models.Relay.query.get(form.name.data) + if conflicting_domain or conflicting_alternative or conflicting_relay: flask.flash('Domain %s is already used' % form.name.data, 'error') else: domain = models.Domain() diff --git a/admin/mailu/admin/views/relays.py b/admin/mailu/admin/views/relays.py new file mode 100644 index 00000000..73a9ad9d --- /dev/null +++ b/admin/mailu/admin/views/relays.py @@ -0,0 +1,60 @@ +from mailu.admin import app, db, models, forms, access +from mailu import app as flask_app + +import flask +import wtforms_components + + +@app.route('/relay', methods=['GET']) +@access.global_admin +def relay_list(): + relays = models.Relay.query.all() + return flask.render_template('relay/list.html', relays=relays) + + +@app.route('/relay/create', methods=['GET', 'POST']) +@access.global_admin +def relay_create(): + form = forms.RelayForm() + if form.validate_on_submit(): + conflicting_domain = models.Domain.query.get(form.name.data) + conflicting_alternative = models.Alternative.query.get(form.name.data) + conflicting_relay = models.Relay.query.get(form.name.data) + if conflicting_domain or conflicting_alternative or conflicting_relay: + flask.flash('Domain %s is already used' % form.name.data, 'error') + else: + relay = models.Relay() + form.populate_obj(relay) + db.session.add(relay) + db.session.commit() + flask.flash('Relayed domain %s created' % relay) + return flask.redirect(flask.url_for('.relay_list')) + return flask.render_template('relay/create.html', form=form) + + +@app.route('/relay/edit/', methods=['GET', 'POST']) +@access.global_admin +def relay_edit(relay_name): + relay = models.Relay.query.get(relay_name) or flask.abort(404) + form = forms.RelayForm(obj=relay) + wtforms_components.read_only(form.name) + form.name.validators = [] + if form.validate_on_submit(): + form.populate_obj(relay) + db.session.commit() + flask.flash('Relayed domain %s saved' % relay) + return flask.redirect(flask.url_for('.relay_list')) + return flask.render_template('relay/edit.html', form=form, + relay=relay) + + +@app.route('/relay/delete/', methods=['GET', 'POST']) +@access.global_admin +@access.confirmation_required("delete {relay_name}") +def relay_delete(relay_name): + relay = models.Relay.query.get(relay_name) or flask.abort(404) + db.session.delete(relay) + db.session.commit() + flask.flash('Relayed domain %s deleted' % relay) + return flask.redirect(flask.url_for('.relay_list')) + diff --git a/admin/migrations/versions/c162ac88012a_.py b/admin/migrations/versions/c162ac88012a_.py new file mode 100644 index 00000000..914ba662 --- /dev/null +++ b/admin/migrations/versions/c162ac88012a_.py @@ -0,0 +1,29 @@ +""" Add relayed domains + +Revision ID: c162ac88012a +Revises: c9a0b4e653cf +Create Date: 2017-09-10 20:21:10.011969 + +""" + +# revision identifiers, used by Alembic. +revision = 'c162ac88012a' +down_revision = 'c9a0b4e653cf' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('relay', + 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('smtp', sa.String(length=80), nullable=True), + sa.PrimaryKeyConstraint('name') + ) + + +def downgrade(): + op.drop_table('relay') diff --git a/postfix/conf/main.cf b/postfix/conf/main.cf index b806a46f..8c8aef0b 100644 --- a/postfix/conf/main.cf +++ b/postfix/conf/main.cf @@ -90,7 +90,8 @@ virtual_alias_maps = ${sql}sqlite-virtual_alias_maps.cf virtual_mailbox_domains = ${sql}sqlite-virtual_mailbox_domains.cf virtual_mailbox_maps = $virtual_alias_maps -# Mails are forwarded to Dovecot for delivery +# Mails are transported if required, then forwarded to Dovecot for delivery +transport_maps = ${sql}sqlite-transport.cf virtual_transport = lmtp:inet:imap:2525 # In order to prevent Postfix from running DNS query, enforce the use of the diff --git a/postfix/conf/sqlite-transport.cf b/postfix/conf/sqlite-transport.cf new file mode 100644 index 00000000..6295523b --- /dev/null +++ b/postfix/conf/sqlite-transport.cf @@ -0,0 +1,3 @@ +dbpath = /data/main.db +query = + SELECT 'smtp:['||smtp||']' FROM relay WHERE name='%s'