Repository: Mailu/Mailu

Issue:
Mail relaying (Relayed domain) does't work when no Remote Host is configured.
Description: 
When in the Web Administration Interface in Relayed domain, a domain is added without a Remote Host, emails for this domain fail to be relayed. When no Remote Host is configured, Mailu is supposed to relay the email message directly to the relayed domain.

In case it is relevant I use the docker-compose deployment.

Replication steps:
1). Deploy a standard Mailu installation (no special configuration required). 
2). In the Web Administration interface, on Relayed domains page add a new relayed domain with
Relayed domain name: test.mailu.io
Remote Host:  (blank, keep it empty).
3). Configure your email client SMTP settings to use your Mailu server on port 25 without any authentication.
4). Send an email to admin@test.mailu.io. If you receive the message, server is busy try again. Then Rspamd has greylisted you. Try sending the email again in 10 to 15 minutes. It should be accepted now.
5). Check the postfix logs (docker logs --tail 100 mailu_smtp_1 )

Aug 13 22:47:32 mail postfix/smtpd[270]: A1C493E2CE1: client=<redacted>.nl[xx.xxx.xx.xxx]
Aug 13 22:47:32 mail postfix/cleanup[315]: A1C493E2CE1: message-id=<71874e8e-c30d-e0a3-4d70-de4542b47664@yahoo.com>
Aug 13 22:47:33 mail postfix/qmgr[177]: A1C493E2CE1: from=<redacted@yahoo.com>, size=2765, nrcpt=1 (queue active)
Aug 13 22:47:33 mail postfix/smtpd[270]: disconnect from <redacted>.nl[xx.xxx.xx.xxx] ehlo=2 xclient=0/1 mail=1 rcpt=1 data=1 quit=1 commands=6/7
**Aug 13 22:47:33 mail postfix/smtp[316]: fatal: valid hostname or network address required in server description: []
Aug 13 22:47:34 mail postfix/qmgr[177]: warning: private/smtp socket: malformed response**
Aug 13 22:47:34 mail postfix/qmgr[177]: warning: transport smtp failure -- see a previous warning/fatal/panic logfile record for the problem description
Aug 13 22:47:34 mail postfix/master[175]: warning: process /usr/libexec/postfix/smtp pid 316 exit status 1
Aug 13 22:47:34 mail postfix/master[175]: warning: /usr/libexec/postfix/smtp: bad command startup -- throttling
Aug 13 22:47:34 mail postfix/error[318]: A1C493E2CE1: to=<admin@test.mailu.io>, relay=none, delay=1.6, delays=0.59/1/0/0.01, dsn=4.3.0, status=deferred (unknown mail transport error)

Workaround:
A workaround is to enter the hostname or IP address of the SMTP server of the relayed domain name as Remote Host. No MX lookup is performed for a hostname entered as Remote Host. For a public domain, you could use the mxtoolbox to lookup the SMTP server. E.g. in my example for test.mailu.io https://mxtoolbox.com/SuperTool.aspx?action=mx%3atest.mailu.io&run=toolpage .

Entering 78.47.92.244 or test.mailu.io as Remote Host will work as a workaround for the above example.
Can relay domains support relaying directly to a LMTP port?
## Before you open your issue
- [x] Check if no issue or pull-request for this already exists.
- [x] Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- [x] You understand `Mailu` is made by volunteers in their **free time** — be conscise, civil and accept that delays can occur.
- [x] The title of the issue should be short and simple. It should contain specific terms related to the actual issue. Be specific while writing the title.

## Environment & Versions

### Environment
 - [ ] docker-compose
 - [ ] kubernetes
 - [x] docker swarm

### Versions

We're using the `master` docker tag.

## Description

When using relay domains in the web interface, the implementation assumes the use of SMTP. However, I am trying to relay directly using another protocol, namely, LMTP (the receiver is a mailman3 instance). 

When I see the remote host of my relay domain to `<host>:8024`, I see `refused to talk to me: 500 Error: command "HELO" not recognized)`. The Mailu postfix relay configuration assumes another postfix on the receiving end.

In my postfix configuration, I see it is possible to support a map such as `lmtp:/<ip>:<port>` and then postfix understands not to start with a `EHLO` command and pass the mail using LMTP (as far as I understand).

So, would it be possible to support LMTP relaying using relay domains?

## Replication Steps

- Setup any relay domain
- Specify a receiving port of 8024
- Setup a receiver on port 8024 expecting LMTP-based requests

## Expected behaviour

Relay domains can support LMTP.

## Logs

```
mail_example_com_smtp.1.zh0tmwq3qnjn@autonomic-swarm    | May 23 09:21:25 mail postfix/smtp[378]: 63D4536B052: to=<doesitwork@lists.example.com>, relay=lists_example_com_core[10.0.44.41]:8024, delay=1.4, delays=1.3/0.08/0.01/0, dsn=5.0.0, status=bounced (host lists_example_com_core[10.0.52.41] refused to talk to me: 500 Error: command "HELO" not recognized)
```



Discussion:
Hi There,

The `Mailu`-Project is currently in a bit of a bind! We are short on man-power, and we need to judge if it is possible for us to put in some work on this issue.

To help with that, we are currently trying to find out which issues are actively keeping users from using `Mailu`, which issues have someone who want to work on them — and which issues may be less important. These a less important ones could be discarded for the time being, until the project is in a more stable and regular state once again.

In order for us to better assess this, it would be helpful if you could put a **reaction on this post** (use the :smiley: icon to the top-right).
- 👍️ if you **need this** to be able to use Mailu. Ideally, you’d also be able to test this on your installation, and provide feedback …
- 🎉 if you find it a **nice bonus**, but no deal-breaker
- 🚀 if you want to **work on it yourself**!
We want to keep this **voting open for 2 weeks** from now, so please help out!
I have two installatioin of mailu server accordingly with documentation. Apparently when I add a relayed domain and I press the Save button nothing seems to work, just not able to add relayed domains.


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
@@ -2,6 +2,7 @@
 from mailu.internal import internal
 
 import flask
+import idna
 import re
 import srslib
 
@@ -35,13 +36,67 @@ def postfix_alias_map(alias):
 def postfix_transport(email):
     if email == '*' or re.match("(^|.*@)\[.*\]$", email):
         return flask.abort(404)
-    localpart, domain_name = models.Email.resolve_domain(email)
+    _, domain_name = models.Email.resolve_domain(email)
     relay = models.Relay.query.get(domain_name) or flask.abort(404)
-    ret = "smtp:[{0}]".format(relay.smtp)
-    if ":" in relay.smtp:
-        split = relay.smtp.split(':')
-        ret = "smtp:[{0}]:{1}".format(split[0], split[1])
-    return flask.jsonify(ret)
+    target = relay.smtp.lower()
+    port = None
+    use_lmtp = False
+    use_mx = False
+    # strip prefixes mx: and lmtp:
+    if target.startswith('mx:'):
+        target = target[3:]
+        use_mx = True
+    elif target.startswith('lmtp:'):
+        target = target[5:]
+        use_lmtp = True
+    # split host:port or [host]:port
+    if target.startswith('['):
+        if use_mx or ']' not in target:
+            # invalid target (mx: and [] or missing ])
+            flask.abort(400)
+        host, rest = target[1:].split(']', 1)
+        if rest.startswith(':'):
+            port = rest[1:]
+        elif rest:
+            # invalid target (rest should be :port)
+            flask.abort(400)
+    else:
+        if ':' in target:
+            host, port = target.rsplit(':', 1)
+        else:
+            host = target
+    # default for empty host part is mx:domain
+    if not host:
+        if not use_lmtp:
+            host = relay.name.lower()
+            use_mx = True
+        else:
+            # lmtp: needs a host part
+            flask.abort(400)
+    # detect ipv6 address or encode host
+    if ':' in host:
+        host = f'ipv6:{host}'
+    else:
+        try:
+            host = idna.encode(host).decode('ascii')
+        except idna.IDNAError:
+            # invalid host (fqdn not encodable)
+            flask.abort(400)
+    # validate port
+    if port is not None:
+        try:
+            port = int(port, 10)
+        except ValueError:
+            # invalid port (should be numeric)
+            flask.abort(400)
+    # create transport
+    transport = 'lmtp' if use_lmtp else 'smtp'
+    # use [] when not using MX lookups or host is an ipv6 address
+    if host.startswith('ipv6:') or (not use_lmtp and not use_mx):
+        host = f'[{host}]'
+    # create port suffix
+    port = '' if port is None else f':{port}'
+    return flask.jsonify(f'{transport}:{host}{port}')
 
 
 @internal.route("/postfix/recipient/map/<path:recipient>")


Test patch: