From 0954fd50fb962fd4bb85439c5183acea7350b3c4 Mon Sep 17 00:00:00 2001 From: Fabian Letzkus Date: Sun, 25 Jun 2017 16:12:44 +0000 Subject: [PATCH 01/15] Added configuration paramter to replace clamav with none --- rmilter/rmilter-clamav.conf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 rmilter/rmilter-clamav.conf diff --git a/rmilter/rmilter-clamav.conf b/rmilter/rmilter-clamav.conf new file mode 100644 index 00000000..b8f38f5e --- /dev/null +++ b/rmilter/rmilter-clamav.conf @@ -0,0 +1,18 @@ +clamav { + # servers - clamav socket definitions in format: + servers = antivirus:3310; + # connect_timeout - timeout in miliseconds for connecting to clamav + connect_timeout = 1s; + # port_timeout - timeout in miliseconds for waiting for clamav port response + port_timeout = 4s; + # results_timeout - timeout in miliseconds for waiting for clamav response + results_timeout = 20s; + # error_time - time in seconds during which we are counting errors + error_time = 10; + # dead_time - time in seconds during which we are thinking that server is down + dead_time = 300; + # maxerrors - maximum number of errors that can occur during error_time to make us thinking that + # Default: 10 + maxerrors = 10; +}; + From 117e858df3148e27947b662ff633290979b42aa1 Mon Sep 17 00:00:00 2001 From: Fabian Letzkus Date: Sun, 25 Jun 2017 16:15:47 +0000 Subject: [PATCH 02/15] Added parameter checking in rmilter image, added parameter in docker-compose, added parameter in .env --- .env.dist | 3 +++ docker-compose.yml.dist | 2 +- rmilter/Dockerfile | 1 + rmilter/rmilter.conf | 18 ------------------ rmilter/start.sh | 4 ++++ 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.env.dist b/.env.dist index 1a1f3b67..8ad95aba 100644 --- a/.env.dist +++ b/.env.dist @@ -51,6 +51,9 @@ EXPOSE_ADMIN=no # Dav server implementation (value: radicale, none) WEBDAV=none +# Antivirus solution (value: none, clamav) +ANTIVIRUS=clamav + ################################### # Mail settings ################################### diff --git a/docker-compose.yml.dist b/docker-compose.yml.dist index 8b6ae76d..ca809af6 100644 --- a/docker-compose.yml.dist +++ b/docker-compose.yml.dist @@ -70,7 +70,7 @@ services: antivirus: # build: clamav - image: mailu/clamav:$VERSION + image: mailu/$ANTIVIRUS:$VERSION restart: always env_file: .env volumes: diff --git a/rmilter/Dockerfile b/rmilter/Dockerfile index 9f474de7..950abbe5 100644 --- a/rmilter/Dockerfile +++ b/rmilter/Dockerfile @@ -4,6 +4,7 @@ RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/re && apk add --no-cache rmilter@testing rsyslog COPY rmilter.conf /etc/rmilter.conf +COPY rmilter-clamav.conf /etc/rmilter-clamav.conf COPY rsyslog.conf /etc/rsyslog.conf COPY start.sh /start.sh diff --git a/rmilter/rmilter.conf b/rmilter/rmilter.conf index 991adf9b..28ef6593 100644 --- a/rmilter/rmilter.conf +++ b/rmilter/rmilter.conf @@ -20,24 +20,6 @@ strict_auth = no; use_dcc = no; use_redis = yes; -clamav { - # servers - clamav socket definitions in format: - servers = antivirus:3310; - # connect_timeout - timeout in miliseconds for connecting to clamav - connect_timeout = 1s; - # port_timeout - timeout in miliseconds for waiting for clamav port response - port_timeout = 4s; - # results_timeout - timeout in miliseconds for waiting for clamav response - results_timeout = 20s; - # error_time - time in seconds during which we are counting errors - error_time = 10; - # dead_time - time in seconds during which we are thinking that server is down - dead_time = 300; - # maxerrors - maximum number of errors that can occur during error_time to make us thinking that - # Default: 10 - maxerrors = 10; -}; - spamd { # servers - spamd socket definitions in format: servers = r:antispam:11333; diff --git a/rmilter/start.sh b/rmilter/start.sh index 2521ea98..d333a86a 100755 --- a/rmilter/start.sh +++ b/rmilter/start.sh @@ -1,5 +1,9 @@ #!/bin/sh rm -f /var/run/rsyslogd.pid +if [ "$ANTIVIRUS" == "clamav" ]; +then + echo "# .try_include /etc/rmilter-clamav.conf" >> /etc/rmilter.conf +fi rmilter -c /etc/rmilter.conf rsyslogd -n From 483443804255bfe114b6a351650ce3335e808459 Mon Sep 17 00:00:00 2001 From: Jaume Martin Date: Sun, 27 Aug 2017 20:24:15 +0200 Subject: [PATCH 03/15] Update .env.dist --- .env.dist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.dist b/.env.dist index 1a1f3b67..84eabf28 100644 --- a/.env.dist +++ b/.env.dist @@ -29,6 +29,10 @@ HOSTNAME=mail.mailu.io # Postmaster local part (will append the main mail domain) POSTMASTER=admin +# DMARC rua and ruf email +DMARC_RUA=admin +DMARC_RUF=admin + # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu From 5b6cba3b9e1051f4c68459b94b6cec97e1981f05 Mon Sep 17 00:00:00 2001 From: Jaume Martin Date: Sun, 27 Aug 2017 20:29:04 +0200 Subject: [PATCH 04/15] Update details.html --- admin/mailu/admin/templates/domain/details.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/admin/mailu/admin/templates/domain/details.html b/admin/mailu/admin/templates/domain/details.html index 9a98f944..ba77201f 100644 --- a/admin/mailu/admin/templates/domain/details.html +++ b/admin/mailu/admin/templates/domain/details.html @@ -42,7 +42,15 @@ {% trans %}DNS DMARC entry{% endtrans %} + {% if config["DMARC_RUA"] != "" and config["DMARC_RUF"] != "" %} +
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }}; ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
+ {% elif config["DMARC_RUA"] != "" %} +
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
+ {% elif config["DMARC_RUF"] != "" %} +
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
+ {% else %}
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["POSTMASTER"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
+ {% endif %} {% endif %} From f823f1e8a5ef4762089b1090b90cbdd83cc1eed1 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 3 Sep 2017 18:30:00 +0200 Subject: [PATCH 05/15] Add the ability to configure alternative domains --- admin/mailu/admin/__init__.py | 1 + admin/mailu/admin/forms.py | 5 ++ admin/mailu/admin/models.py | 16 +++++++ .../admin/templates/alternative/create.html | 9 ++++ .../admin/templates/alternative/list.html | 34 ++++++++++++++ admin/mailu/admin/templates/domain/list.html | 3 ++ admin/mailu/admin/views/alternatives.py | 46 +++++++++++++++++++ admin/mailu/admin/views/domains.py | 4 +- admin/migrations/versions/c9a0b4e653cf_.py | 30 ++++++++++++ 9 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 admin/mailu/admin/templates/alternative/create.html create mode 100644 admin/mailu/admin/templates/alternative/list.html create mode 100644 admin/mailu/admin/views/alternatives.py create mode 100644 admin/migrations/versions/c9a0b4e653cf_.py 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') From 64ded60b5e8de1d929f6f46ec5d685b99f329a83 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 15:30:25 +0200 Subject: [PATCH 06/15] Implement the postfix part of domain aliasing --- postfix/conf/sqlite-virtual_alias_maps.cf | 4 +++- postfix/conf/sqlite-virtual_mailbox_domains.cf | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) 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' From d90b7444c826265384bc96601426154ca7394192 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 19:28:59 +0200 Subject: [PATCH 07/15] Move settings to the email section --- .env.dist | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.dist b/.env.dist index 7393474f..58bfc145 100644 --- a/.env.dist +++ b/.env.dist @@ -29,10 +29,6 @@ HOSTNAME=mail.mailu.io # Postmaster local part (will append the main mail domain) POSTMASTER=admin -# DMARC rua and ruf email -DMARC_RUA=admin -DMARC_RUF=admin - # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu @@ -81,6 +77,10 @@ FETCHMAIL_DELAY=600 # e.g. localpart+custom@domain;tld RECIPIENT_DELIMITER=+ +# DMARC rua and ruf email +DMARC_RUA=admin +DMARC_RUF=admin + ################################### # Nginx settings ################################### From 9754fffbc410706d41ce33baa5bac3747608eb9c Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 19:35:31 +0200 Subject: [PATCH 08/15] Simplify the details page for RUA/RUF --- admin/mailu/admin/templates/domain/details.html | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/admin/mailu/admin/templates/domain/details.html b/admin/mailu/admin/templates/domain/details.html index ba77201f..1ae45e16 100644 --- a/admin/mailu/admin/templates/domain/details.html +++ b/admin/mailu/admin/templates/domain/details.html @@ -42,15 +42,7 @@ {% trans %}DNS DMARC entry{% endtrans %} - {% if config["DMARC_RUA"] != "" and config["DMARC_RUF"] != "" %} -
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }}; ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
- {% elif config["DMARC_RUA"] != "" %} -
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
- {% elif config["DMARC_RUF"] != "" %} -
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
- {% else %} -
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["POSTMASTER"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"
- {% endif %} +
_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject;{% if config["DMARC_RUA"] %} rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }};{% endif %}{% if config["DMARC_RUF"] %} ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }};{% endif %} adkim=s; aspf=s"
{% endif %} From 03b352cbcd857dc748f05e2ea3ce592ee22b180c Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 19:35:43 +0200 Subject: [PATCH 09/15] Declare the DMARC RUA/RUF configuration items --- admin/mailu/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/admin/mailu/__init__.py b/admin/mailu/__init__.py index aeb4d89e..d09bb889 100644 --- a/admin/mailu/__init__.py +++ b/admin/mailu/__init__.py @@ -27,6 +27,8 @@ default_config = { 'BOOTSTRAP_SERVE_LOCAL': True, 'DKIM_PATH': '/dkim/{domain}.{selector}.key', 'DKIM_SELECTOR': 'dkim', + 'DMARC_RUA': None, + 'DMARC_RUF': None, 'BABEL_DEFAULT_LOCALE': 'en', 'BABEL_DEFAULT_TIMEZONE': 'UTC', 'ENABLE_CERTBOT': False, From a092e3f9b23f27f623d3e1515654c1ef89589a3d Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 19:42:54 +0200 Subject: [PATCH 10/15] Properly include the rmilter clamav configuration --- rmilter/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmilter/start.sh b/rmilter/start.sh index 98927d04..ccd32981 100755 --- a/rmilter/start.sh +++ b/rmilter/start.sh @@ -10,7 +10,7 @@ done rm -f /var/run/rsyslogd.pid if [ "$ANTIVIRUS" == "clamav" ]; then - echo "# .try_include /etc/rmilter-clamav.conf" >> /etc/rmilter.conf + echo ".try_include /etc/rmilter-clamav.conf" >> /etc/rmilter.conf fi rmilter -c /etc/rmilter.conf rsyslogd -n From b5c55d10d35b4bbbc99ce6fba6660c9a8f33df3a Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 20:30:03 +0200 Subject: [PATCH 11/15] Add the relay concept to the model --- admin/mailu/admin/models.py | 14 +++++++++++ admin/migrations/versions/c162ac88012a_.py | 29 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 admin/migrations/versions/c162ac88012a_.py 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/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') From 01ddfef149f906abe1326f4016898c7edc3fb775 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 20:48:39 +0200 Subject: [PATCH 12/15] Implement relayed domains in the admin interface --- admin/mailu/admin/__init__.py | 1 + admin/mailu/admin/forms.py | 7 +++ admin/mailu/admin/templates/relay/create.html | 5 ++ admin/mailu/admin/templates/relay/edit.html | 9 +++ admin/mailu/admin/templates/relay/list.html | 39 ++++++++++++ admin/mailu/admin/templates/sidebar.html | 5 ++ admin/mailu/admin/views/relays.py | 60 +++++++++++++++++++ 7 files changed, 126 insertions(+) create mode 100644 admin/mailu/admin/templates/relay/create.html create mode 100644 admin/mailu/admin/templates/relay/edit.html create mode 100644 admin/mailu/admin/templates/relay/list.html create mode 100644 admin/mailu/admin/views/relays.py 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/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/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')) + From d4254bfd1f1b0e87c2d2d32ddb1b4985853f4ebc Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 10 Sep 2017 20:49:49 +0200 Subject: [PATCH 13/15] Avoid duplicating relays with alternatives or domains --- admin/mailu/admin/views/alternatives.py | 3 ++- admin/mailu/admin/views/domains.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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() From 6b7a33c43599fea1f06c9a68101d80ccca1c40ca Mon Sep 17 00:00:00 2001 From: kaiyou Date: Mon, 11 Sep 2017 08:12:04 +0200 Subject: [PATCH 14/15] Add a (not working) postfix conf for transports --- postfix/conf/main.cf | 3 ++- postfix/conf/sqlite-transport.cf | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 postfix/conf/sqlite-transport.cf 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..2f9de981 --- /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' From c4eeaad6767ea2b9ed16eaf3349a73fe9b23ee36 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Mon, 11 Sep 2017 20:31:14 +0200 Subject: [PATCH 15/15] Disable MX queries when relaying mail to a remote smtp --- postfix/conf/sqlite-transport.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postfix/conf/sqlite-transport.cf b/postfix/conf/sqlite-transport.cf index 2f9de981..6295523b 100644 --- a/postfix/conf/sqlite-transport.cf +++ b/postfix/conf/sqlite-transport.cf @@ -1,3 +1,3 @@ dbpath = /data/main.db query = - SELECT 'smtp:'||smtp FROM relay WHERE name='%s' + SELECT 'smtp:['||smtp||']' FROM relay WHERE name='%s'