Repository: Mailu/Mailu

Issue:
DNS DKIM entry is broken up in two parts (for 2048b keys), making it error-prone
<!--

Thank you for opening an issue with Mailu. Please understand that issues are meant for bugs and enhancement-requests.
For **user-support questions**, reach out to us  on [matrix](https://matrix.to/#/#mailu:tedomum.net).

To be able to help you best, we need some more information.

Before you open your issue
- Check if no issue or pull-request for this already exists.
- Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- You understand `Mailu` is made by volunteers in their **free time** — be concise, civil and accept that delays can occur.
- 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.

Please put your text outside of the comment blocks to be visible. You can use the button "Preview" above to check.

-->

## Environment & Version

### Environment

- [x] docker-compose
- [ ] kubernetes
- [ ] docker swarm

### Version

- Version: 1.9

<!--
To find your version, get the image name of a mailu container and read  the version from the tag (example for version 1.7).

$> docker ps -a | grep mailu
140b09d4b09c    mailu/roundcube:1.7    "docker-php-entrypoi…"    2 weeks ago    Up 2 days (healthy)    80/tcp
$> grep MAILU_VERSION docker-compose.yml mailu.env
-->

## Description
<!--
Further explain the bug in a few words. It should be clear what the unexpected behaviour is.  Share it in an easy-to-understand language.
-->
DNS DKIM entry in mailu-admin is broken into two parts for 2048b keys.

## Replication Steps
<!--
Steps for replicating your issue
-->

1. Set up a mail domain
2. Generate keys in admin
3. Observe DNS DKIM entry

## Observed behaviour
<!--
Explain or paste the result you received.
-->

```
[...] 600 IN TXT "v=DKIM1; k=rsa; p=[.....]" "[......]"
```

(the " " in between is hard to notice, and easily make people just copy that into the dns configuration, rendering the dkim entry invalid)

## Expected behaviour
<!--
Explain what results you expected - be as specific as possible.
Just saying "it doesn’t work as expected" is not useful. It's also helpful to describe what you actually experienced.
-->

```
[...] 600 IN TXT "v=DKIM1; k=rsa; p=[.....][......]"
```

(shouldn't be broken up into two parts)

## Logs
<!--
Often it is very useful to include log fragments of the involved component.
You can get the logs via `docker logs <container name> --tail 1000`.
For example for the admin container: `docker logs mailu_admin_1 --tail 1000`
or using docker-compose `docker-compose -f /mailu/docker-compose.yml logs --tail 1000 admin`

If you can find the relevant section, please share only the parts that seem relevant. If you have any logs, please enclose them in code tags, like so:

```
Your logs here!
```
-->
not relevant


Discussion:
@tusooa You can read here https://www.mailhardener.com/blog/how-to-enter-txt-values-in-google-cloud-dns why we do this intentionally.
Maybe adding a toggle which splits/joins the field (using javascript) would be a solution for everyone.
Hm... making it break everywhere in order to not break in Google Cloud DNS is a bit suboptimal (especially because those are reported as bugs there and were not closed by Google).

I'd add instructions on how to split to FAQ instead (there are apparently tools like https://www.mailhardener.com/tools/dns-record-splitter for this) and leave it contiguous the way it should be for everyone else.
As a new user, i lost an entire day troubleshooting this issue..
kubernetes user here btw
i was trying to setup the entries in a cloudflare dns manually, i was not really sure the correct way it was supposed to be inserted..

i eventually found out somewhere that i could paste the entries in a text file and upload to cloudflare and it created the correct entries... 
maybe at least we could get a button to generate the text file so we can just import the entries into the dns provider?

or better yet if the correct implementation is only to solve an issue with 1 specific dns provider, don't make the solution for 1, be the solution for everybody else..
maybe put a note for that specific provider..

i also wanted to leave a note, probably should create my own issue...
in the same page the MX entry has a green mark saying its "ok"
for some time while troubleshooting i was hoping the other entries showed up with a green mark as well
I'm currently compiling a PR with interfaces changes (auto-focus of form fields, better navigation, dns validation, ...)
I'll add the non-splitted version there (show both or make toggling possible, sth like this.)

For now you can output all the keys using the config-export (which makes it easy to automate the update using a dns api):
```
docker compose exec admin flask mailu config-export -jd domain | jq -r '.domain[] | select(.dkim_publickey) | "dkim._domainkey.\(.name). 600 IN TXT \"v=DKIM1; k=rsa; p=\(.dkim_publickey)\""'
```

Resolving patch:
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
@@ -232,9 +232,7 @@ def dns_dkim(self):
         """ return DKIM record for domain """
         if self.dkim_key:
             selector = app.config['DKIM_SELECTOR']
-            txt = f'v=DKIM1; k=rsa; p={self.dkim_publickey}'
-            record = ' '.join(f'"{txt[p:p+250]}"' for p in range(0, len(txt), 250))
-            return f'{selector}._domainkey.{self.name}. 600 IN TXT {record}'
+            return f'{selector}._domainkey.{self.name}. 600 IN TXT "v=DKIM1; k=rsa; p={self.dkim_publickey}"'
 
     @cached_property
     def dns_dmarc(self):
diff --git a/core/admin/mailu/ui/views/domains.py b/core/admin/mailu/ui/views/domains.py
--- a/core/admin/mailu/ui/views/domains.py
+++ b/core/admin/mailu/ui/views/domains.py
@@ -70,6 +70,27 @@ def domain_details(domain_name):
     domain = models.Domain.query.get(domain_name) or flask.abort(404)
     return flask.render_template('domain/details.html', domain=domain)
 
+@ui.route('/domain/details/<domain_name>/zonefile', methods=['GET'])
+@access.domain_admin(models.Domain, 'domain_name')
+def domain_download_zonefile(domain_name):
+    domain = models.Domain.query.get(domain_name) or flask.abort(404)
+    res = [domain.dns_mx, domain.dns_spf]
+    if domain.dkim_publickey:
+        record = domain.dns_dkim.split('"', 1)[0].strip()
+        txt = f'v=DKIM1; k=rsa; p={domain.dkim_publickey}'
+        txt = ' '.join(f'"{txt[p:p+250]}"' for p in range(0, len(txt), 250))
+        res.append(f'{record} {txt}')
+        res.append(domain.dns_dmarc)
+    if domain.dns_tlsa:
+        res.append(domain.dns_tlsa)
+    res.extend(domain.dns_autoconfig)
+    res.append("")
+    return flask.Response(
+        "\n".join(res),
+        content_type="text/plain",
+        headers={"Content-disposition": f"attachment; filename={domain.name}-zonefile.txt"}
+    )
+
 
 @ui.route('/domain/genkeys/<domain_name>', methods=['GET', 'POST'])
 @access.domain_admin(models.Domain, 'domain_name')


Test patch: