Repository: Mailu/Mailu

Issue:
Can't send from an email account that has forwarding
I've setup an email account (for example  "myname@example.com").

I then turn on forwarding so that all emails to "myname@example.com" will be forwarded to "mygmail@gmail.com".

However when doing this I can no longer send emails from "myname@example.com", when doing (either via SMTP directly or roundcube) I get error such as:

> An error occurred while sending mail. The mail server responded:  
> 5.7.1 <myname@example.com>: Sender address rejected: not owned by user myname@example.com.
>  Please check the message recipient "someoneelse@yahoo.com" and try again.


Discussion:
Sorry for the delay. Indeed the sender restrictions will cause an issue with this. For the time being, try checking the box "keep a local copy".
The reasoning is: if your sender address does not have your username as destination, it is rejected. So keeping a local copy will make your local user a recipient of the messages and thus temporarily fix the issue.

I have to think about a long term fix, which will not be easy.
This can finally be fixed once we implement #420. Then we can finally specify any behavior for allowing impersonation, I will start with fixing the current ux.

Resolving patch:
diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py
--- a/core/admin/mailu/internal/views/postfix.py
+++ b/core/admin/mailu/internal/views/postfix.py
@@ -18,37 +18,34 @@ def postfix_mailbox_map(email):
 
 @internal.route("/postfix/alias/<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/<email>")
 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)
+    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))
 
 
-@internal.route("/postfix/sender/<sender>")
-def postfix_sender(sender):
+@internal.route("/postfix/sender/login/<sender>")
+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/<sender>")
+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
--- a/core/admin/mailu/models.py
+++ b/core/admin/mailu/models.py
@@ -64,7 +64,7 @@ def process_bind_param(self, value, dialect):
         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
@@ -221,6 +221,28 @@ def sendmail(self, subject, body):
             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 = 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 = Alias.resolve(localpart, domain_name)
+        if alias:
+            return alias.destination
+        user = 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=[])
     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)
diff --git a/core/postfix/start.py b/core/postfix/start.py
--- a/core/postfix/start.py
+++ b/core/postfix/start.py
@@ -19,7 +19,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))


Test patch: