We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
dnstools dkim TXT record evaluation fix
Description of the issue/feature this PR addresses:
The DKIM button was red, but my TXT record setted well. So i looked about the issue, and i founded the evaluation of the TXT record is not good. According to DNS standard, a TXT record what is longer than 256 character need to split to several strings: https://help.directadmin.com/item.php?id=552

Current behavior before PR:
I just merge the splitted parts together.

Desired behavior after PR is merged:
The DKIM button is now green as it should be.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `modoboa/dnstools/lib.py`
Content:
```
1 """dnstools library."""
2 
3 import ipaddress
4 
5 from django.utils.translation import ugettext as _
6 
7 from modoboa.admin import lib as admin_lib
8 
9 from . import constants
10 
11 
12 def get_spf_record(domain):
13     """Return SPF record for domain (if any)."""
14     records = admin_lib.get_dns_records(domain, "TXT")
15     if records is None:
16         return None
17     for record in records:
18         value = str(record).strip('"')
19         if value.startswith("v=spf1"):
20             return value
21     return None
22 
23 
24 def get_dkim_record(domain, selector):
25     """Return DKIM records form domain (if any)."""
26     name = "{}._domainkey.{}".format(selector, domain)
27     records = admin_lib.get_dns_records(name, "TXT")
28     if records is None:
29         return None
30     for record in records:
31         value = str(record).strip('"')
32         if value.startswith("v=DKIM1"):
33             return value
34     return None
35 
36 
37 def get_dmarc_record(domain):
38     """Return DMARC record for domain (if any)."""
39     name = "_dmarc.{}".format(domain)
40     records = admin_lib.get_dns_records(name, "TXT")
41     if records is None:
42         return None
43     for record in records:
44         value = str(record).strip('"')
45         if value.startswith("v=DMARC1"):
46             return value
47     return None
48 
49 
50 def _get_simple_record(name):
51     """We just want to know if name is declared."""
52     for rdtype in ["A", "CNAME", "AAAA"]:
53         records = admin_lib.get_dns_records(name, rdtype)
54         if records is not None:
55             break
56     else:
57         return None
58     for record in records:
59         value = str(record).strip('"')
60         break
61     return value
62 
63 
64 def get_autoconfig_record(domain):
65     """Return autoconfig record for domain (if any)."""
66     return _get_simple_record("autoconfig.{}".format(domain))
67 
68 
69 def get_autodiscover_record(domain):
70     """Return autodiscover record for domain (if any)."""
71     return _get_simple_record("autodiscover.{}".format(domain))
72 
73 
74 class DNSSyntaxError(Exception):
75     """Custom exception for DNS errors."""
76 
77     pass
78 
79 
80 def check_spf_ip4(value):
81     """Check syntax of ip4 mechanism."""
82     parts = value.split(":")
83     if len(parts) != 2:
84         raise DNSSyntaxError(_("Wrong ip4 mechanism syntax"))
85     try:
86         ipaddress.ip_network(parts[1], False)
87     except ValueError:
88         raise DNSSyntaxError(_("Wrong IPv4 address format"))
89 
90 
91 def check_spf_ip6(value):
92     """Check syntax of ip6 mechanism."""
93     if not value.startswith("ip6:"):
94         raise DNSSyntaxError(_("Wrong ip6 mechanism syntax"))
95     value = value.replace("ip6:", "")
96     try:
97         ipaddress.ip_network(value, False)
98     except ValueError:
99         raise DNSSyntaxError(_("Wrong IPv6 address format"))
100 
101 
102 def _check_domain_and_mask(value, mechanism):
103     """Check for valid domain / mask."""
104     domain = None
105     mask = None
106     if ":" in value:
107         mechanism, domain = value.split(":")
108         if "/" in domain:
109             domain, mask = domain.split("/")
110     elif "/" in value:
111         mechanism, mask = value.split("/")
112     else:
113         raise DNSSyntaxError(
114             _("Invalid syntax for {} mechanism").format(mechanism))
115     if mask and (not mask.isdigit() or int(mask) > 32):
116         raise DNSSyntaxError(_("Invalid mask found {}").format(mask))
117 
118 
119 def check_spf_a(value):
120     """Check syntax of a mechanism."""
121     if value == "a":
122         return
123     _check_domain_and_mask(value, "a")
124 
125 
126 def check_spf_mx(value):
127     """Check syntax of mx mechanism."""
128     if value == "mx":
129         return
130     _check_domain_and_mask(value, "mx")
131 
132 
133 def _check_simple(value, mechanism):
134     """Simple check."""
135     if value == mechanism:
136         return
137     parts = value.split(":")
138     if len(parts) != 2:
139         raise DNSSyntaxError(
140             _("Invalid syntax for {} mechanism").format(mechanism))
141 
142 
143 def check_spf_ptr(value):
144     """Check syntax of ptr mechanism."""
145     _check_simple(value, "ptr")
146 
147 
148 def check_spf_exists(value):
149     """Check syntax of ptr mechanism."""
150     _check_simple(value, "exists")
151 
152 
153 def check_spf_include(value):
154     """Check syntax of include mechanism."""
155     _check_simple(value, "include")
156 
157 
158 def check_spf_syntax(record):
159     """Check if record has a valid SPF syntax."""
160     if not record.startswith("v=spf1"):
161         raise DNSSyntaxError(_("Not an SPF record"))
162     parts = record.split(" ")[1:]
163     modifiers = []
164     mechanisms = []
165     for part in parts:
166         if part == "":
167             continue
168         qualifier = None
169         if part[0] in ["+", "-", "~", "?"]:
170             qualifier = part[0]
171             part = part[1:]
172         if part == "all":
173             continue
174         for mechanism in constants.SPF_MECHANISMS:
175             if part.startswith(mechanism):
176                 globals()["check_spf_{}".format(mechanism)](part)
177                 mechanisms.append(mechanism)
178                 break
179         else:
180             # Look for modifier
181             modifier = part.split("=")
182             if len(modifier) != 2:
183                 raise DNSSyntaxError(_("Unknown mechanism {}").format(part))
184             if modifier[0] not in ["redirect", "exp"]:
185                 raise DNSSyntaxError(_("Unknown modifier {}").format(
186                     modifier[0]))
187             if modifier[0] in modifiers:
188                 raise DNSSyntaxError(_("Duplicate modifier {} found").format(
189                     modifier[0]))
190             modifiers.append(modifier[0])
191     if not len(mechanisms) and not len(modifiers):
192         raise DNSSyntaxError(_("No mechanism found"))
193     return None
194 
195 
196 def check_dkim_syntax(record):
197     """Check if record has a valid DKIM syntax."""
198     if not record.startswith("v=DKIM1"):
199         raise DNSSyntaxError(_("Not a valid DKIM record"))
200     key = None
201     for tag in record.split(";")[1:]:
202         tag = tag.strip(" ")
203         if tag == "":
204             continue
205         parts = tag.split("=", 1)
206         if len(parts) != 2:
207             raise DNSSyntaxError(_("Invalid tag {}").format(tag))
208         name = parts[0].strip(" ")
209         if name == "p":
210             key = "".join(part.strip('"') for part in parts[1].split(" "))
211     if key is None:
212         raise DNSSyntaxError(_("No key found in record"))
213     return key
214 
215 
216 def check_dmarc_tag_string_value(tag, value):
217     """Check if value is valid for tag."""
218     tdef = constants.DMARC_TAGS[tag]
219     error = _("Wrong value {} for tag {}").format(value, tag)
220     if "values" in tdef and value not in tdef["values"]:
221         raise DNSSyntaxError(error)
222     elif "regex" in tdef and tdef["regex"].match(value) is None:
223         raise DNSSyntaxError(error)
224 
225 
226 def check_dmarc_tag(tag, value):
227     """Check if tag is valid."""
228     tdef = constants.DMARC_TAGS[tag]
229     ttype = tdef.get("type", "string")
230     if ttype == "list":
231         for svalue in value.split(","):
232             check_dmarc_tag_string_value(tag, svalue)
233     elif ttype == "int":
234         error = _("Wrong value {} for tag {}:").format(value, tag)
235         try:
236             value = int(value)
237         except ValueError:
238             raise DNSSyntaxError(error + _(" not an integer"))
239         if "min_value" in tdef and value < tdef["min_value"]:
240             raise DNSSyntaxError(
241                 error + _(" less than {}").format(tdef["min_value"]))
242         if "max_value" in tdef and value > tdef["max_value"]:
243             raise DNSSyntaxError(
244                 error + _(" greater than {}").format(tdef["max_value"]))
245     else:
246         check_dmarc_tag_string_value(tag, value)
247 
248 
249 def check_dmarc_syntax(record):
250     """Check if record has a valid DMARC syntax."""
251     if not record.startswith("v=DMARC1"):
252         raise DNSSyntaxError(_("Not a valid DMARC record"))
253     tags = {}
254     for tag in record.split(";")[1:]:
255         if tag == "":
256             continue
257         tag = tag.strip(" ")
258         parts = tag.split("=")
259         if len(parts) != 2:
260             raise DNSSyntaxError(_("Invalid tag {}").format(tag))
261         name = parts[0].strip(" ")
262         if name not in constants.DMARC_TAGS:
263             raise DNSSyntaxError(_("Unknown tag {}").format(name))
264         value = parts[1].strip(" ")
265         check_dmarc_tag(name, value)
266         tags[name] = value
267     if "p" not in tags:
268         raise DNSSyntaxError(_("Missing required p tag"))
269
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/modoboa/dnstools/lib.py b/modoboa/dnstools/lib.py
--- a/modoboa/dnstools/lib.py
+++ b/modoboa/dnstools/lib.py
@@ -9,42 +9,36 @@
 from . import constants
 
 
-def get_spf_record(domain):
-    """Return SPF record for domain (if any)."""
-    records = admin_lib.get_dns_records(domain, "TXT")
+def _get_record_type_value(records, rr_type):
     if records is None:
         return None
     for record in records:
-        value = str(record).strip('"')
-        if value.startswith("v=spf1"):
+        # Multiple strings are separated by a space as described in:
+        # https://tools.ietf.org/html/rfc4408#section-3.1.3
+        value = str(record).replace('" "', '').strip('"')
+        if value.startswith('v={}'.format(rr_type)):
             return value
     return None
 
 
+def get_spf_record(domain):
+    """Return SPF record for domain (if any)."""
+    records = admin_lib.get_dns_records(domain, "TXT")
+    return _get_record_type_value(records, 'spf1')
+
+
 def get_dkim_record(domain, selector):
     """Return DKIM records form domain (if any)."""
     name = "{}._domainkey.{}".format(selector, domain)
     records = admin_lib.get_dns_records(name, "TXT")
-    if records is None:
-        return None
-    for record in records:
-        value = str(record).strip('"')
-        if value.startswith("v=DKIM1"):
-            return value
-    return None
+    return _get_record_type_value(records, 'DKIM1')
 
 
 def get_dmarc_record(domain):
     """Return DMARC record for domain (if any)."""
     name = "_dmarc.{}".format(domain)
     records = admin_lib.get_dns_records(name, "TXT")
-    if records is None:
-        return None
-    for record in records:
-        value = str(record).strip('"')
-        if value.startswith("v=DMARC1"):
-            return value
-    return None
+    return _get_record_type_value(records, 'DMARC1')
 
 
 def _get_simple_record(name):

{"golden_diff": "diff --git a/modoboa/dnstools/lib.py b/modoboa/dnstools/lib.py\n--- a/modoboa/dnstools/lib.py\n+++ b/modoboa/dnstools/lib.py\n@@ -9,42 +9,36 @@\n from . import constants\n \n \n-def get_spf_record(domain):\n-    \"\"\"Return SPF record for domain (if any).\"\"\"\n-    records = admin_lib.get_dns_records(domain, \"TXT\")\n+def _get_record_type_value(records, rr_type):\n     if records is None:\n         return None\n     for record in records:\n-        value = str(record).strip('\"')\n-        if value.startswith(\"v=spf1\"):\n+        # Multiple strings are separated by a space as described in:\n+        # https://tools.ietf.org/html/rfc4408#section-3.1.3\n+        value = str(record).replace('\" \"', '').strip('\"')\n+        if value.startswith('v={}'.format(rr_type)):\n             return value\n     return None\n \n \n+def get_spf_record(domain):\n+    \"\"\"Return SPF record for domain (if any).\"\"\"\n+    records = admin_lib.get_dns_records(domain, \"TXT\")\n+    return _get_record_type_value(records, 'spf1')\n+\n+\n def get_dkim_record(domain, selector):\n     \"\"\"Return DKIM records form domain (if any).\"\"\"\n     name = \"{}._domainkey.{}\".format(selector, domain)\n     records = admin_lib.get_dns_records(name, \"TXT\")\n-    if records is None:\n-        return None\n-    for record in records:\n-        value = str(record).strip('\"')\n-        if value.startswith(\"v=DKIM1\"):\n-            return value\n-    return None\n+    return _get_record_type_value(records, 'DKIM1')\n \n \n def get_dmarc_record(domain):\n     \"\"\"Return DMARC record for domain (if any).\"\"\"\n     name = \"_dmarc.{}\".format(domain)\n     records = admin_lib.get_dns_records(name, \"TXT\")\n-    if records is None:\n-        return None\n-    for record in records:\n-        value = str(record).strip('\"')\n-        if value.startswith(\"v=DMARC1\"):\n-            return value\n-    return None\n+    return _get_record_type_value(records, 'DMARC1')\n \n \n def _get_simple_record(name):\n", "issue": "dnstools dkim TXT record evaluation fix\nDescription of the issue/feature this PR addresses:\r\nThe DKIM button was red, but my TXT record setted well. So i looked about the issue, and i founded the evaluation of the TXT record is not good. According to DNS standard, a TXT record what is longer than 256 character need to split to several strings: https://help.directadmin.com/item.php?id=552\r\n\r\nCurrent behavior before PR:\r\nI just merge the splitted parts together.\r\n\r\nDesired behavior after PR is merged:\r\nThe DKIM button is now green as it should be.\n", "before_files": [{"content": "\"\"\"dnstools library.\"\"\"\n\nimport ipaddress\n\nfrom django.utils.translation import ugettext as _\n\nfrom modoboa.admin import lib as admin_lib\n\nfrom . import constants\n\n\ndef get_spf_record(domain):\n    \"\"\"Return SPF record for domain (if any).\"\"\"\n    records = admin_lib.get_dns_records(domain, \"TXT\")\n    if records is None:\n        return None\n    for record in records:\n        value = str(record).strip('\"')\n        if value.startswith(\"v=spf1\"):\n            return value\n    return None\n\n\ndef get_dkim_record(domain, selector):\n    \"\"\"Return DKIM records form domain (if any).\"\"\"\n    name = \"{}._domainkey.{}\".format(selector, domain)\n    records = admin_lib.get_dns_records(name, \"TXT\")\n    if records is None:\n        return None\n    for record in records:\n        value = str(record).strip('\"')\n        if value.startswith(\"v=DKIM1\"):\n            return value\n    return None\n\n\ndef get_dmarc_record(domain):\n    \"\"\"Return DMARC record for domain (if any).\"\"\"\n    name = \"_dmarc.{}\".format(domain)\n    records = admin_lib.get_dns_records(name, \"TXT\")\n    if records is None:\n        return None\n    for record in records:\n        value = str(record).strip('\"')\n        if value.startswith(\"v=DMARC1\"):\n            return value\n    return None\n\n\ndef _get_simple_record(name):\n    \"\"\"We just want to know if name is declared.\"\"\"\n    for rdtype in [\"A\", \"CNAME\", \"AAAA\"]:\n        records = admin_lib.get_dns_records(name, rdtype)\n        if records is not None:\n            break\n    else:\n        return None\n    for record in records:\n        value = str(record).strip('\"')\n        break\n    return value\n\n\ndef get_autoconfig_record(domain):\n    \"\"\"Return autoconfig record for domain (if any).\"\"\"\n    return _get_simple_record(\"autoconfig.{}\".format(domain))\n\n\ndef get_autodiscover_record(domain):\n    \"\"\"Return autodiscover record for domain (if any).\"\"\"\n    return _get_simple_record(\"autodiscover.{}\".format(domain))\n\n\nclass DNSSyntaxError(Exception):\n    \"\"\"Custom exception for DNS errors.\"\"\"\n\n    pass\n\n\ndef check_spf_ip4(value):\n    \"\"\"Check syntax of ip4 mechanism.\"\"\"\n    parts = value.split(\":\")\n    if len(parts) != 2:\n        raise DNSSyntaxError(_(\"Wrong ip4 mechanism syntax\"))\n    try:\n        ipaddress.ip_network(parts[1], False)\n    except ValueError:\n        raise DNSSyntaxError(_(\"Wrong IPv4 address format\"))\n\n\ndef check_spf_ip6(value):\n    \"\"\"Check syntax of ip6 mechanism.\"\"\"\n    if not value.startswith(\"ip6:\"):\n        raise DNSSyntaxError(_(\"Wrong ip6 mechanism syntax\"))\n    value = value.replace(\"ip6:\", \"\")\n    try:\n        ipaddress.ip_network(value, False)\n    except ValueError:\n        raise DNSSyntaxError(_(\"Wrong IPv6 address format\"))\n\n\ndef _check_domain_and_mask(value, mechanism):\n    \"\"\"Check for valid domain / mask.\"\"\"\n    domain = None\n    mask = None\n    if \":\" in value:\n        mechanism, domain = value.split(\":\")\n        if \"/\" in domain:\n            domain, mask = domain.split(\"/\")\n    elif \"/\" in value:\n        mechanism, mask = value.split(\"/\")\n    else:\n        raise DNSSyntaxError(\n            _(\"Invalid syntax for {} mechanism\").format(mechanism))\n    if mask and (not mask.isdigit() or int(mask) > 32):\n        raise DNSSyntaxError(_(\"Invalid mask found {}\").format(mask))\n\n\ndef check_spf_a(value):\n    \"\"\"Check syntax of a mechanism.\"\"\"\n    if value == \"a\":\n        return\n    _check_domain_and_mask(value, \"a\")\n\n\ndef check_spf_mx(value):\n    \"\"\"Check syntax of mx mechanism.\"\"\"\n    if value == \"mx\":\n        return\n    _check_domain_and_mask(value, \"mx\")\n\n\ndef _check_simple(value, mechanism):\n    \"\"\"Simple check.\"\"\"\n    if value == mechanism:\n        return\n    parts = value.split(\":\")\n    if len(parts) != 2:\n        raise DNSSyntaxError(\n            _(\"Invalid syntax for {} mechanism\").format(mechanism))\n\n\ndef check_spf_ptr(value):\n    \"\"\"Check syntax of ptr mechanism.\"\"\"\n    _check_simple(value, \"ptr\")\n\n\ndef check_spf_exists(value):\n    \"\"\"Check syntax of ptr mechanism.\"\"\"\n    _check_simple(value, \"exists\")\n\n\ndef check_spf_include(value):\n    \"\"\"Check syntax of include mechanism.\"\"\"\n    _check_simple(value, \"include\")\n\n\ndef check_spf_syntax(record):\n    \"\"\"Check if record has a valid SPF syntax.\"\"\"\n    if not record.startswith(\"v=spf1\"):\n        raise DNSSyntaxError(_(\"Not an SPF record\"))\n    parts = record.split(\" \")[1:]\n    modifiers = []\n    mechanisms = []\n    for part in parts:\n        if part == \"\":\n            continue\n        qualifier = None\n        if part[0] in [\"+\", \"-\", \"~\", \"?\"]:\n            qualifier = part[0]\n            part = part[1:]\n        if part == \"all\":\n            continue\n        for mechanism in constants.SPF_MECHANISMS:\n            if part.startswith(mechanism):\n                globals()[\"check_spf_{}\".format(mechanism)](part)\n                mechanisms.append(mechanism)\n                break\n        else:\n            # Look for modifier\n            modifier = part.split(\"=\")\n            if len(modifier) != 2:\n                raise DNSSyntaxError(_(\"Unknown mechanism {}\").format(part))\n            if modifier[0] not in [\"redirect\", \"exp\"]:\n                raise DNSSyntaxError(_(\"Unknown modifier {}\").format(\n                    modifier[0]))\n            if modifier[0] in modifiers:\n                raise DNSSyntaxError(_(\"Duplicate modifier {} found\").format(\n                    modifier[0]))\n            modifiers.append(modifier[0])\n    if not len(mechanisms) and not len(modifiers):\n        raise DNSSyntaxError(_(\"No mechanism found\"))\n    return None\n\n\ndef check_dkim_syntax(record):\n    \"\"\"Check if record has a valid DKIM syntax.\"\"\"\n    if not record.startswith(\"v=DKIM1\"):\n        raise DNSSyntaxError(_(\"Not a valid DKIM record\"))\n    key = None\n    for tag in record.split(\";\")[1:]:\n        tag = tag.strip(\" \")\n        if tag == \"\":\n            continue\n        parts = tag.split(\"=\", 1)\n        if len(parts) != 2:\n            raise DNSSyntaxError(_(\"Invalid tag {}\").format(tag))\n        name = parts[0].strip(\" \")\n        if name == \"p\":\n            key = \"\".join(part.strip('\"') for part in parts[1].split(\" \"))\n    if key is None:\n        raise DNSSyntaxError(_(\"No key found in record\"))\n    return key\n\n\ndef check_dmarc_tag_string_value(tag, value):\n    \"\"\"Check if value is valid for tag.\"\"\"\n    tdef = constants.DMARC_TAGS[tag]\n    error = _(\"Wrong value {} for tag {}\").format(value, tag)\n    if \"values\" in tdef and value not in tdef[\"values\"]:\n        raise DNSSyntaxError(error)\n    elif \"regex\" in tdef and tdef[\"regex\"].match(value) is None:\n        raise DNSSyntaxError(error)\n\n\ndef check_dmarc_tag(tag, value):\n    \"\"\"Check if tag is valid.\"\"\"\n    tdef = constants.DMARC_TAGS[tag]\n    ttype = tdef.get(\"type\", \"string\")\n    if ttype == \"list\":\n        for svalue in value.split(\",\"):\n            check_dmarc_tag_string_value(tag, svalue)\n    elif ttype == \"int\":\n        error = _(\"Wrong value {} for tag {}:\").format(value, tag)\n        try:\n            value = int(value)\n        except ValueError:\n            raise DNSSyntaxError(error + _(\" not an integer\"))\n        if \"min_value\" in tdef and value < tdef[\"min_value\"]:\n            raise DNSSyntaxError(\n                error + _(\" less than {}\").format(tdef[\"min_value\"]))\n        if \"max_value\" in tdef and value > tdef[\"max_value\"]:\n            raise DNSSyntaxError(\n                error + _(\" greater than {}\").format(tdef[\"max_value\"]))\n    else:\n        check_dmarc_tag_string_value(tag, value)\n\n\ndef check_dmarc_syntax(record):\n    \"\"\"Check if record has a valid DMARC syntax.\"\"\"\n    if not record.startswith(\"v=DMARC1\"):\n        raise DNSSyntaxError(_(\"Not a valid DMARC record\"))\n    tags = {}\n    for tag in record.split(\";\")[1:]:\n        if tag == \"\":\n            continue\n        tag = tag.strip(\" \")\n        parts = tag.split(\"=\")\n        if len(parts) != 2:\n            raise DNSSyntaxError(_(\"Invalid tag {}\").format(tag))\n        name = parts[0].strip(\" \")\n        if name not in constants.DMARC_TAGS:\n            raise DNSSyntaxError(_(\"Unknown tag {}\").format(name))\n        value = parts[1].strip(\" \")\n        check_dmarc_tag(name, value)\n        tags[name] = value\n    if \"p\" not in tags:\n        raise DNSSyntaxError(_(\"Missing required p tag\"))\n", "path": "modoboa/dnstools/lib.py"}], "after_files": [{"content": "\"\"\"dnstools library.\"\"\"\n\nimport ipaddress\n\nfrom django.utils.translation import ugettext as _\n\nfrom modoboa.admin import lib as admin_lib\n\nfrom . import constants\n\n\ndef _get_record_type_value(records, rr_type):\n    if records is None:\n        return None\n    for record in records:\n        # Multiple strings are separated by a space as described in:\n        # https://tools.ietf.org/html/rfc4408#section-3.1.3\n        value = str(record).replace('\" \"', '').strip('\"')\n        if value.startswith('v={}'.format(rr_type)):\n            return value\n    return None\n\n\ndef get_spf_record(domain):\n    \"\"\"Return SPF record for domain (if any).\"\"\"\n    records = admin_lib.get_dns_records(domain, \"TXT\")\n    return _get_record_type_value(records, 'spf1')\n\n\ndef get_dkim_record(domain, selector):\n    \"\"\"Return DKIM records form domain (if any).\"\"\"\n    name = \"{}._domainkey.{}\".format(selector, domain)\n    records = admin_lib.get_dns_records(name, \"TXT\")\n    return _get_record_type_value(records, 'DKIM1')\n\n\ndef get_dmarc_record(domain):\n    \"\"\"Return DMARC record for domain (if any).\"\"\"\n    name = \"_dmarc.{}\".format(domain)\n    records = admin_lib.get_dns_records(name, \"TXT\")\n    return _get_record_type_value(records, 'DMARC1')\n\n\ndef _get_simple_record(name):\n    \"\"\"We just want to know if name is declared.\"\"\"\n    for rdtype in [\"A\", \"CNAME\", \"AAAA\"]:\n        records = admin_lib.get_dns_records(name, rdtype)\n        if records is not None:\n            break\n    else:\n        return None\n    for record in records:\n        value = str(record).strip('\"')\n        break\n    return value\n\n\ndef get_autoconfig_record(domain):\n    \"\"\"Return autoconfig record for domain (if any).\"\"\"\n    return _get_simple_record(\"autoconfig.{}\".format(domain))\n\n\ndef get_autodiscover_record(domain):\n    \"\"\"Return autodiscover record for domain (if any).\"\"\"\n    return _get_simple_record(\"autodiscover.{}\".format(domain))\n\n\nclass DNSSyntaxError(Exception):\n    \"\"\"Custom exception for DNS errors.\"\"\"\n\n    pass\n\n\ndef check_spf_ip4(value):\n    \"\"\"Check syntax of ip4 mechanism.\"\"\"\n    parts = value.split(\":\")\n    if len(parts) != 2:\n        raise DNSSyntaxError(_(\"Wrong ip4 mechanism syntax\"))\n    try:\n        ipaddress.ip_network(parts[1], False)\n    except ValueError:\n        raise DNSSyntaxError(_(\"Wrong IPv4 address format\"))\n\n\ndef check_spf_ip6(value):\n    \"\"\"Check syntax of ip6 mechanism.\"\"\"\n    if not value.startswith(\"ip6:\"):\n        raise DNSSyntaxError(_(\"Wrong ip6 mechanism syntax\"))\n    value = value.replace(\"ip6:\", \"\")\n    try:\n        ipaddress.ip_network(value, False)\n    except ValueError:\n        raise DNSSyntaxError(_(\"Wrong IPv6 address format\"))\n\n\ndef _check_domain_and_mask(value, mechanism):\n    \"\"\"Check for valid domain / mask.\"\"\"\n    domain = None\n    mask = None\n    if \":\" in value:\n        mechanism, domain = value.split(\":\")\n        if \"/\" in domain:\n            domain, mask = domain.split(\"/\")\n    elif \"/\" in value:\n        mechanism, mask = value.split(\"/\")\n    else:\n        raise DNSSyntaxError(\n            _(\"Invalid syntax for {} mechanism\").format(mechanism))\n    if mask and (not mask.isdigit() or int(mask) > 32):\n        raise DNSSyntaxError(_(\"Invalid mask found {}\").format(mask))\n\n\ndef check_spf_a(value):\n    \"\"\"Check syntax of a mechanism.\"\"\"\n    if value == \"a\":\n        return\n    _check_domain_and_mask(value, \"a\")\n\n\ndef check_spf_mx(value):\n    \"\"\"Check syntax of mx mechanism.\"\"\"\n    if value == \"mx\":\n        return\n    _check_domain_and_mask(value, \"mx\")\n\n\ndef _check_simple(value, mechanism):\n    \"\"\"Simple check.\"\"\"\n    if value == mechanism:\n        return\n    parts = value.split(\":\")\n    if len(parts) != 2:\n        raise DNSSyntaxError(\n            _(\"Invalid syntax for {} mechanism\").format(mechanism))\n\n\ndef check_spf_ptr(value):\n    \"\"\"Check syntax of ptr mechanism.\"\"\"\n    _check_simple(value, \"ptr\")\n\n\ndef check_spf_exists(value):\n    \"\"\"Check syntax of ptr mechanism.\"\"\"\n    _check_simple(value, \"exists\")\n\n\ndef check_spf_include(value):\n    \"\"\"Check syntax of include mechanism.\"\"\"\n    _check_simple(value, \"include\")\n\n\ndef check_spf_syntax(record):\n    \"\"\"Check if record has a valid SPF syntax.\"\"\"\n    if not record.startswith(\"v=spf1\"):\n        raise DNSSyntaxError(_(\"Not an SPF record\"))\n    parts = record.split(\" \")[1:]\n    modifiers = []\n    mechanisms = []\n    for part in parts:\n        if part == \"\":\n            continue\n        qualifier = None\n        if part[0] in [\"+\", \"-\", \"~\", \"?\"]:\n            qualifier = part[0]\n            part = part[1:]\n        if part == \"all\":\n            continue\n        for mechanism in constants.SPF_MECHANISMS:\n            if part.startswith(mechanism):\n                globals()[\"check_spf_{}\".format(mechanism)](part)\n                mechanisms.append(mechanism)\n                break\n        else:\n            # Look for modifier\n            modifier = part.split(\"=\")\n            if len(modifier) != 2:\n                raise DNSSyntaxError(_(\"Unknown mechanism {}\").format(part))\n            if modifier[0] not in [\"redirect\", \"exp\"]:\n                raise DNSSyntaxError(_(\"Unknown modifier {}\").format(\n                    modifier[0]))\n            if modifier[0] in modifiers:\n                raise DNSSyntaxError(_(\"Duplicate modifier {} found\").format(\n                    modifier[0]))\n            modifiers.append(modifier[0])\n    if not len(mechanisms) and not len(modifiers):\n        raise DNSSyntaxError(_(\"No mechanism found\"))\n    return None\n\n\ndef check_dkim_syntax(record):\n    \"\"\"Check if record has a valid DKIM syntax.\"\"\"\n    if not record.startswith(\"v=DKIM1\"):\n        raise DNSSyntaxError(_(\"Not a valid DKIM record\"))\n    key = None\n    for tag in record.split(\";\")[1:]:\n        tag = tag.strip(\" \")\n        if tag == \"\":\n            continue\n        parts = tag.split(\"=\", 1)\n        if len(parts) != 2:\n            raise DNSSyntaxError(_(\"Invalid tag {}\").format(tag))\n        name = parts[0].strip(\" \")\n        if name == \"p\":\n            key = \"\".join(part.strip('\"') for part in parts[1].split(\" \"))\n    if key is None:\n        raise DNSSyntaxError(_(\"No key found in record\"))\n    return key\n\n\ndef check_dmarc_tag_string_value(tag, value):\n    \"\"\"Check if value is valid for tag.\"\"\"\n    tdef = constants.DMARC_TAGS[tag]\n    error = _(\"Wrong value {} for tag {}\").format(value, tag)\n    if \"values\" in tdef and value not in tdef[\"values\"]:\n        raise DNSSyntaxError(error)\n    elif \"regex\" in tdef and tdef[\"regex\"].match(value) is None:\n        raise DNSSyntaxError(error)\n\n\ndef check_dmarc_tag(tag, value):\n    \"\"\"Check if tag is valid.\"\"\"\n    tdef = constants.DMARC_TAGS[tag]\n    ttype = tdef.get(\"type\", \"string\")\n    if ttype == \"list\":\n        for svalue in value.split(\",\"):\n            check_dmarc_tag_string_value(tag, svalue)\n    elif ttype == \"int\":\n        error = _(\"Wrong value {} for tag {}:\").format(value, tag)\n        try:\n            value = int(value)\n        except ValueError:\n            raise DNSSyntaxError(error + _(\" not an integer\"))\n        if \"min_value\" in tdef and value < tdef[\"min_value\"]:\n            raise DNSSyntaxError(\n                error + _(\" less than {}\").format(tdef[\"min_value\"]))\n        if \"max_value\" in tdef and value > tdef[\"max_value\"]:\n            raise DNSSyntaxError(\n                error + _(\" greater than {}\").format(tdef[\"max_value\"]))\n    else:\n        check_dmarc_tag_string_value(tag, value)\n\n\ndef check_dmarc_syntax(record):\n    \"\"\"Check if record has a valid DMARC syntax.\"\"\"\n    if not record.startswith(\"v=DMARC1\"):\n        raise DNSSyntaxError(_(\"Not a valid DMARC record\"))\n    tags = {}\n    for tag in record.split(\";\")[1:]:\n        if tag == \"\":\n            continue\n        tag = tag.strip(\" \")\n        parts = tag.split(\"=\")\n        if len(parts) != 2:\n            raise DNSSyntaxError(_(\"Invalid tag {}\").format(tag))\n        name = parts[0].strip(\" \")\n        if name not in constants.DMARC_TAGS:\n            raise DNSSyntaxError(_(\"Unknown tag {}\").format(name))\n        value = parts[1].strip(\" \")\n        check_dmarc_tag(name, value)\n        tags[name] = value\n    if \"p\" not in tags:\n        raise DNSSyntaxError(_(\"Missing required p tag\"))\n", "path": "modoboa/dnstools/lib.py"}]}