mailauth-records 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.dotfiles
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
4
|
+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
5
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
6
|
+
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
9
|
+
the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mailauth-records
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Parse SPF, DMARC, MTA-STS and TLS-RPT DNS records into plain Python data (no dependencies)
|
|
5
|
+
Author-email: mailcheck <pypi-n5t87@tuxguard.com>, mailcheck <pypi-nrklb@tuxguard.de>
|
|
6
|
+
Maintainer-email: mailcheck <pypi-4fu64@tuxred.de>, mailcheck <pypi-btnse@ozapftis.cloud>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: dmarc,dns,email,mta-sts,spf,tls-rpt
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Communications :: Email
|
|
12
|
+
Classifier: Topic :: Internet :: Name Service (DNS)
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# mailauth-records
|
|
17
|
+
|
|
18
|
+
Parse mail-authentication DNS records into plain Python data. No dependencies.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from mailauth_records import parse_dmarc, parse_spf
|
|
22
|
+
|
|
23
|
+
parse_dmarc("v=DMARC1; p=reject; rua=mailto:dmarc@example.org")
|
|
24
|
+
# {'v': 'DMARC1', 'p': 'reject', 'rua': ['mailto:dmarc@example.org'], 'sp': 'reject', 'adkim': 'r', 'aspf': 'r', 'pct': 100}
|
|
25
|
+
|
|
26
|
+
parse_spf("v=spf1 mx ip4:192.0.2.0/24 -all")["all"] # '-'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Supported: SPF (RFC 7208), DMARC (RFC 7489), MTA-STS (RFC 8461), TLS-RPT (RFC 8460).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# mailauth-records
|
|
2
|
+
|
|
3
|
+
Parse mail-authentication DNS records into plain Python data. No dependencies.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from mailauth_records import parse_dmarc, parse_spf
|
|
7
|
+
|
|
8
|
+
parse_dmarc("v=DMARC1; p=reject; rua=mailto:dmarc@example.org")
|
|
9
|
+
# {'v': 'DMARC1', 'p': 'reject', 'rua': ['mailto:dmarc@example.org'], 'sp': 'reject', 'adkim': 'r', 'aspf': 'r', 'pct': 100}
|
|
10
|
+
|
|
11
|
+
parse_spf("v=spf1 mx ip4:192.0.2.0/24 -all")["all"] # '-'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Supported: SPF (RFC 7208), DMARC (RFC 7489), MTA-STS (RFC 8461), TLS-RPT (RFC 8460).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mailauth-records"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Parse SPF, DMARC, MTA-STS and TLS-RPT DNS records into plain Python data (no dependencies)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "mailcheck", email = "pypi-n5t87@tuxguard.com" },
|
|
14
|
+
{ name = "mailcheck", email = "pypi-nrklb@tuxguard.de" },
|
|
15
|
+
]
|
|
16
|
+
maintainers = [
|
|
17
|
+
{ name = "mailcheck", email = "pypi-4fu64@tuxred.de" },
|
|
18
|
+
{ name = "mailcheck", email = "pypi-btnse@ozapftis.cloud" },
|
|
19
|
+
]
|
|
20
|
+
keywords = ["dmarc", "spf", "mta-sts", "tls-rpt", "email", "dns"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Topic :: Communications :: Email",
|
|
24
|
+
"Topic :: Internet :: Name Service (DNS)",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/mailauth_records"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Parse mail-authentication DNS records (SPF, DMARC, MTA-STS, TLS-RPT) into plain Python data.
|
|
2
|
+
|
|
3
|
+
>>> parse_dmarc("v=DMARC1; p=reject; rua=mailto:dmarc@example.org; adkim=s")["p"]
|
|
4
|
+
'reject'
|
|
5
|
+
>>> parse_spf("v=spf1 mx ip4:192.0.2.0/24 -all")["all"]
|
|
6
|
+
'-'
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__ = ["parse_tags", "parse_dmarc", "parse_spf", "parse_mta_sts", "parse_tlsrpt"]
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_tags(record: str) -> dict[str, str]:
|
|
15
|
+
"""Generic 'k=v; k=v' tag list (DMARC, MTA-STS, TLS-RPT). Keys are lower-cased, values stripped."""
|
|
16
|
+
out: dict[str, str] = {}
|
|
17
|
+
for part in record.split(";"):
|
|
18
|
+
if "=" in part:
|
|
19
|
+
k, v = part.split("=", 1)
|
|
20
|
+
out[k.strip().lower()] = v.strip()
|
|
21
|
+
return out
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_dmarc(record: str) -> dict:
|
|
25
|
+
"""DMARC record -> dict with defaults (RFC 7489: p required, sp=p, adkim/aspf=r, pct=100)."""
|
|
26
|
+
t = parse_tags(record)
|
|
27
|
+
if t.get("v", "").upper() != "DMARC1":
|
|
28
|
+
raise ValueError("not a DMARC record")
|
|
29
|
+
t.setdefault("sp", t.get("p", "none"))
|
|
30
|
+
t.setdefault("adkim", "r")
|
|
31
|
+
t.setdefault("aspf", "r")
|
|
32
|
+
t["pct"] = int(t.get("pct", "100"))
|
|
33
|
+
for k in ("rua", "ruf"):
|
|
34
|
+
if k in t:
|
|
35
|
+
t[k] = [u.strip() for u in t[k].split(",") if u.strip()]
|
|
36
|
+
return t
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_spf(record: str) -> dict:
|
|
40
|
+
"""SPF record -> {'mechanisms': [(qualifier, mechanism, value)], 'modifiers': {...}, 'all': qualifier|None}."""
|
|
41
|
+
terms = record.split()
|
|
42
|
+
if not terms or terms[0].lower() != "v=spf1":
|
|
43
|
+
raise ValueError("not an SPF record")
|
|
44
|
+
mechs, mods, allq = [], {}, None
|
|
45
|
+
for term in terms[1:]:
|
|
46
|
+
if "=" in term and ":" not in term.split("=", 1)[0]:
|
|
47
|
+
k, v = term.split("=", 1)
|
|
48
|
+
mods[k.lower()] = v
|
|
49
|
+
continue
|
|
50
|
+
q = term[0] if term[0] in "+-~?" else "+"
|
|
51
|
+
body = term[1:] if term[0] in "+-~?" else term
|
|
52
|
+
name, _, value = body.partition(":")
|
|
53
|
+
if name.lower() == "all":
|
|
54
|
+
allq = q
|
|
55
|
+
mechs.append((q, name.lower(), value or None))
|
|
56
|
+
return {"mechanisms": mechs, "modifiers": mods, "all": allq}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_mta_sts(record: str) -> dict[str, str]:
|
|
60
|
+
"""_mta-sts TXT record (RFC 8461), e.g. 'v=STSv1; id=20240101'."""
|
|
61
|
+
t = parse_tags(record)
|
|
62
|
+
if t.get("v") != "STSv1":
|
|
63
|
+
raise ValueError("not an MTA-STS record")
|
|
64
|
+
return t
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_tlsrpt(record: str) -> dict:
|
|
68
|
+
"""_smtp._tls TXT record (RFC 8460), e.g. 'v=TLSRPTv1; rua=mailto:tls@example.org'."""
|
|
69
|
+
t = parse_tags(record)
|
|
70
|
+
if t.get("v") != "TLSRPTv1":
|
|
71
|
+
raise ValueError("not a TLS-RPT record")
|
|
72
|
+
t["rua"] = [u.strip() for u in t.get("rua", "").split(",") if u.strip()]
|
|
73
|
+
return t
|