From 8b189ed1452fd80a4fa166d0ba6cfc007cdd18e0 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 7 Oct 2018 16:23:53 +0200 Subject: [PATCH 1/7] Separate senderaccess and senderlogin maps --- core/postfix/conf/main.cf | 4 ++-- core/postfix/start.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index cd052d46..12541cc5 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -80,14 +80,14 @@ lmtp_host_lookup = native smtpd_delay_reject = yes # Allowed senders are: the user or one of the alias destinations -smtpd_sender_login_maps = $virtual_alias_maps +smtpd_sender_login_maps = ${podop}senderlogin # Restrictions for incoming SMTP, other restrictions are applied in master.cf smtpd_helo_required = yes smtpd_client_restrictions = permit_mynetworks, - check_sender_access ${podop}sender, + check_sender_access ${podop}senderaccess, reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unknown_recipient_domain, diff --git a/core/postfix/start.py b/core/postfix/start.py index 251f5b05..44ab1b26 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -17,7 +17,8 @@ def start_podop(): ("alias", "url", "http://admin/internal/postfix/alias/§"), ("domain", "url", "http://admin/internal/postfix/domain/§"), ("mailbox", "url", "http://admin/internal/postfix/mailbox/§"), - ("sender", "url", "http://admin/internal/postfix/sender/§") + ("senderaccess", "url", "http://admin/internal/postfix/sender/access/§"), + ("senderlogin", "url", "http://admin/internal/postfix/sender/login/§") ]) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) From 508e519a34f75260673d6ba62e21307ec38dae82 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 7 Oct 2018 16:24:48 +0200 Subject: [PATCH 2/7] Refactor the postfix views and implement sender checks --- core/admin/mailu/internal/views/postfix.py | 43 ++++++++++------------ core/admin/mailu/models.py | 34 +++++++++++------ 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 79fbdb8a..f0fbaa5d 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -18,37 +18,32 @@ def postfix_mailbox_map(email): @internal.route("/postfix/alias/") def postfix_alias_map(alias): - localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias) - alternative = models.Alternative.query.get(domain) - if alternative: - domain = alternative.domain_name - email = '{}@{}'.format(localpart, domain) + localpart, domain_name = models.Email.resolve_domain(alias) if localpart is None: - return flask.jsonify(domain) - else: - alias_obj = models.Alias.resolve(localpart, domain) - if alias_obj: - return flask.jsonify(",".join(alias_obj.destination)) - user_obj = models.User.query.get(email) - if user_obj: - return flask.jsonify(user_obj.destination) - return flask.abort(404) + return flask.jsonify(domain_name) + destination = models.Email.resolve_destination(localpart, domain_name) + return flask.jsonify(",".join(destination)) if destination else flask.abort(404) @internal.route("/postfix/transport/") def postfix_transport(email): - localpart, domain = email.split('@', 1) if '@' in email else (None, email) - relay = models.Relay.query.get(domain) or flask.abort(404) + localpart, domain = models.Email.resolve_domain(email) + relay = models.Relay.query.get(domain_name) or flask.abort(404) return flask.jsonify("smtp:[{}]".format(relay.smtp)) -@internal.route("/postfix/sender/") -def postfix_sender(sender): +@internal.route("/postfix/sender/login/") +def postfix_sender_login(sender): + localpart, domain_name = models.Email.resolve_domain(sender) + if localpart is None: + return flask.abort(404) + destination = models.Email.resolve_destination(localpart, domain_name, True) + return flask.jsonify(",".join(destination)) if destination else flask.abort(404) + + +@internal.route("/postfix/sender/access/") +def postfix_sender_access(sender): """ Simply reject any sender that pretends to be from a local domain """ - localpart, domain_name = sender.split('@', 1) if '@' in sender else (None, sender) - domain = models.Domain.query.get(domain_name) - alternative = models.Alternative.query.get(domain_name) - if domain or alternative: - return flask.jsonify("REJECT") - return flask.abort(404) + localpart, domain_name = models.Email.resolve_domain(sender) + return flask.jsonify("REJECT") if models.Domain.query.get(domain_name) else flask.abort(404) diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 1bcc4e9f..6685fc60 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -221,6 +221,28 @@ class Email(object): msg['To'] = to_address smtp.sendmail(from_address, [to_address], msg.as_string()) + @classmethod + def resolve_domain(cls, email): + localpart, domain_name = email.split('@', 1) if '@' in email else (None, email) + alternative = models.Alternative.query.get(domain_name) + if alternative: + domain_name = alternative.domain_name + return (localpart, domain_name) + + @classmethod + def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False): + alias = models.Alias.resolve(localpart, domain_name) + if alias: + return alias.destination + user = models.User.query.get('{}@{}'.format(localpart, domain_name)) + if user: + if user.forward_enabled: + destination = user.forward_destination + if user.forward_keep or ignore_forward_keep: + destination.append(user.email) + else: + destination = [user.email] + return destination def __str__(self): return self.email @@ -245,7 +267,7 @@ class User(Base, Email): # Filters forward_enabled = db.Column(db.Boolean(), nullable=False, default=False) - forward_destination = db.Column(db.String(255), nullable=True, default=None) + forward_destination = db.Column(CommaSeparatedList(), nullable=True, default=None) forward_keep = db.Column(db.Boolean(), nullable=False, default=True) reply_enabled = db.Column(db.Boolean(), nullable=False, default=False) reply_subject = db.Column(db.String(255), nullable=True, default=None) @@ -266,16 +288,6 @@ class User(Base, Email): def get_id(self): return self.email - @property - def destination(self): - if self.forward_enabled: - result = self.self.forward_destination - if self.forward_keep: - result += ',' + self.email - return result - else: - return self.email - scheme_dict = {'SHA512-CRYPT': "sha512_crypt", 'SHA256-CRYPT': "sha256_crypt", 'MD5-CRYPT': "md5_crypt", From e784556330c385ae73c39c009f62eb619af7a11f Mon Sep 17 00:00:00 2001 From: kaiyou Date: Tue, 16 Oct 2018 20:47:38 +0200 Subject: [PATCH 3/7] Fix an edge case with old values containing None for coma separated lists --- core/admin/mailu/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 6685fc60..63d0e4f9 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -64,7 +64,7 @@ class CommaSeparatedList(db.TypeDecorator): return ",".join(value) def process_result_value(self, value, dialect): - return filter(bool, value.split(",")) + return filter(bool, value.split(",")) if value else [] # Many-to-many association table for domain managers From aed80a74faa241a0b37dea79e81c8cbd3f2420e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 23 Oct 2018 11:52:15 +0300 Subject: [PATCH 4/7] Rectify decleration of domain_name --- core/admin/mailu/internal/views/postfix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index f0fbaa5d..894532a3 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -27,7 +27,7 @@ def postfix_alias_map(alias): @internal.route("/postfix/transport/") def postfix_transport(email): - localpart, domain = models.Email.resolve_domain(email) + localpart, domain_name = models.Email.resolve_domain(email) relay = models.Relay.query.get(domain_name) or flask.abort(404) return flask.jsonify("smtp:[{}]".format(relay.smtp)) From ed81c076f2c635c342e21f08289aa203f00f0b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 23 Oct 2018 11:53:52 +0300 Subject: [PATCH 5/7] Take out "models" path, as we are already in it --- core/admin/mailu/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 2845334e..ffe1ad08 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -224,17 +224,17 @@ class Email(object): @classmethod def resolve_domain(cls, email): localpart, domain_name = email.split('@', 1) if '@' in email else (None, email) - alternative = models.Alternative.query.get(domain_name) + alternative = Alternative.query.get(domain_name) if alternative: domain_name = alternative.domain_name return (localpart, domain_name) @classmethod def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False): - alias = models.Alias.resolve(localpart, domain_name) + alias = Alias.resolve(localpart, domain_name) if alias: return alias.destination - user = models.User.query.get('{}@{}'.format(localpart, domain_name)) + user = User.query.get('{}@{}'.format(localpart, domain_name)) if user: if user.forward_enabled: destination = user.forward_destination From eff6c34632c8c4097c94162b38d5ca97b06ba15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 4 Dec 2018 15:40:07 +0200 Subject: [PATCH 6/7] Catch asterisk before resolve_domain Asterisk results in IDNA error and a 500 return code. --- core/admin/mailu/internal/views/postfix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 894532a3..7343459a 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -27,6 +27,8 @@ def postfix_alias_map(alias): @internal.route("/postfix/transport/") def postfix_transport(email): + if email == '*': + return flask.abort(404) localpart, domain_name = models.Email.resolve_domain(email) relay = models.Relay.query.get(domain_name) or flask.abort(404) return flask.jsonify("smtp:[{}]".format(relay.smtp)) From c9df311a0de4f379a7a9f9f2845b2d7fa01b1828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 4 Dec 2018 16:22:18 +0200 Subject: [PATCH 7/7] Set forward_destination to an empty list The value of `None` resulted in an error, since a list was expected. --- core/admin/mailu/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index ffe1ad08..62577dd5 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -267,7 +267,7 @@ class User(Base, Email): # Filters forward_enabled = db.Column(db.Boolean(), nullable=False, default=False) - forward_destination = db.Column(CommaSeparatedList(), nullable=True, default=None) + forward_destination = db.Column(CommaSeparatedList(), nullable=True, default=[]) forward_keep = db.Column(db.Boolean(), nullable=False, default=True) reply_enabled = db.Column(db.Boolean(), nullable=False, default=False) reply_subject = db.Column(db.String(255), nullable=True, default=None)