We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
SPF parser failure
Parser can fail because of extra space characters.
```python
 File "/srv/modoboa/instance/manage.py", line 25, in <module>
    execute_from_command_line(sys.argv)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 364, in execute_from_command_line
    utility.execute()
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 356, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/subcommand/base.py", line 53, in run_from_argv
    return super(SubcommandCommand, self).run_from_argv(argv)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py", line 283, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py", line 330, in execute
    output = self.handle(*args, **options)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/subcommand/base.py", line 86, in handle
    return command.run_from_argv(argv)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py", line 283, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py", line 330, in execute
    output = self.handle(*args, **options)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/admin/management/commands/subcommands/_mx.py", line 275, in handle
    self.check_domain(domain, **options)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/admin/management/commands/subcommands/_mx.py", line 210, in check_domain
    domain, "spf", ttl)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/dnstools/models.py", line 37, in get_or_create_for_domain
    record.check_syntax(ttl)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/dnstools/models.py", line 74, in check_syntax
    result = func(self.value)
  File "/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/dnstools/lib.py", line 173, in check_spf_syntax
    if part[0] in ["+", "-", "~", "?"]:
IndexError: string index out of range
```

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

{"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@@ -165,6 +165,8 @@\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@@ -200,6 +202,8 @@\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@@ -250,6 +254,8 @@\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", "issue": "SPF parser failure\nParser can fail because of extra space characters.\r\n```python\r\n File \"/srv/modoboa/instance/manage.py\", line 25, in <module>\r\n    execute_from_command_line(sys.argv)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/__init__.py\", line 364, in execute_from_command_line\r\n    utility.execute()\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/__init__.py\", line 356, in execute\r\n    self.fetch_command(subcommand).run_from_argv(self.argv)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/subcommand/base.py\", line 53, in run_from_argv\r\n    return super(SubcommandCommand, self).run_from_argv(argv)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py\", line 283, in run_from_argv\r\n    self.execute(*args, **cmd_options)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py\", line 330, in execute\r\n    output = self.handle(*args, **options)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/subcommand/base.py\", line 86, in handle\r\n    return command.run_from_argv(argv)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py\", line 283, in run_from_argv\r\n    self.execute(*args, **cmd_options)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/django/core/management/base.py\", line 330, in execute\r\n    output = self.handle(*args, **options)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/admin/management/commands/subcommands/_mx.py\", line 275, in handle\r\n    self.check_domain(domain, **options)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/admin/management/commands/subcommands/_mx.py\", line 210, in check_domain\r\n    domain, \"spf\", ttl)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/dnstools/models.py\", line 37, in get_or_create_for_domain\r\n    record.check_syntax(ttl)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/dnstools/models.py\", line 74, in check_syntax\r\n    result = func(self.value)\r\n  File \"/srv/modoboa/env/local/lib/python2.7/site-packages/modoboa/dnstools/lib.py\", line 173, in check_spf_syntax\r\n    if part[0] in [\"+\", \"-\", \"~\", \"?\"]:\r\nIndexError: string index out of range\r\n```\n", "before_files": [{"content": "\"\"\"dnstools library.\"\"\"\n\nfrom __future__ import unicode_literals\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        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        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        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\nfrom __future__ import unicode_literals\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"}]}