disban 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,39 @@
1
+ /bin/
2
+ /dist/
3
+ *.exe
4
+ .env
5
+ .DS_Store
6
+
7
+ # Local recovery snapshots contain account and routing state.
8
+ /data/launch-review/backups/
9
+
10
+ # Authentication/provider credentials
11
+ .env.*
12
+ !.env.example
13
+
14
+ # Temporary local administrator test credential (hash only)
15
+ /web/data/local-test-login.json
16
+
17
+ # Generated datasets, runtime state, backups, and local process output.
18
+ /data/*
19
+ !/data/providers.sample.txt
20
+ /web/data/
21
+ *.sqlite*
22
+ *.db
23
+ *.db-wal
24
+ *.db-shm
25
+ *.log
26
+ *.pid
27
+
28
+ # Dependencies, local tool settings, and standalone build output.
29
+ node_modules/
30
+ /.claude/settings.local.json
31
+ /import-mx
32
+ coverage/
33
+ playwright-report/
34
+ test-results/
35
+ *.tsbuildinfo
36
+
37
+ # Local Python benchmark bytecode.
38
+ __pycache__/
39
+ *.pyc
disban-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.5
2
+ Name: disban
3
+ Version: 0.1.0
4
+ Summary: Detect disposable email at signup: classify an email domain as disposable, free, alias, testing, parked or unknown. Works without an API key.
5
+ Project-URL: Homepage, https://disban.io
6
+ Project-URL: Documentation, https://disban.io/docs
7
+ Project-URL: Support, https://disban.io/support
8
+ Author-email: Disban <info@disban.io>
9
+ License-Expression: MIT
10
+ Keywords: burner-email,disposable-email,email-validation,signup,temporary-email
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Communications :: Email
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+
17
+ # disban
18
+
19
+ Detect disposable email addresses at signup. `disban` classifies the domain of an email address as **disposable**, **free** (Gmail, Outlook), **alias** (privacy relays such as SimpleLogin), **testing**, **parked** or **unknown**, and tells you whether it can receive mail. Works without an API key. Standard library only.
20
+
21
+ ```sh
22
+ pip install disban
23
+ ```
24
+
25
+ ```python
26
+ from disban import is_disposable, signup_decision
27
+
28
+ is_disposable("someone@yopmail.com") # True; None means unknown (not verified safe)
29
+
30
+ decision = signup_decision(email)
31
+ if decision["action"] == "block":
32
+ raise ValueError("Please use a permanent email address.")
33
+ if decision["action"] == "fix":
34
+ raise ValueError("That domain can't receive email. Check for a typo.")
35
+ # "allow": create the account; if decision["hold"], hold free credits until the email is confirmed
36
+ ```
37
+
38
+ `signup_decision` fails open: if Disban is slow, unreachable or rate-limited, it allows the signup with `hold: True`.
39
+
40
+ ## FastAPI
41
+
42
+ ```python
43
+ from fastapi import FastAPI, HTTPException
44
+ from disban import signup_decision
45
+
46
+ app = FastAPI()
47
+
48
+ @app.post("/signup")
49
+ def signup(email: str):
50
+ decision = signup_decision(email)
51
+ if decision["action"] != "allow":
52
+ raise HTTPException(422, decision["reason"])
53
+ return {"ok": True, "review": decision.get("hold", False)}
54
+ ```
55
+
56
+ ## Limits and API keys
57
+
58
+ Without a key: 10 checks an hour and 30 a day per network, from your server. For production, set `DISBAN_API_KEY`: a free key includes **2,000 checks a month** with the evidence behind each result. [disban.io/signup](https://disban.io/signup)
59
+
60
+ ```sh
61
+ disban check someone@mailinator.com example.com
62
+ ```
63
+
64
+ [Docs](https://disban.io/docs) · [Methodology](https://disban.io/methodology) · [llms.txt](https://disban.io/llms.txt)
disban-0.1.0/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # disban
2
+
3
+ Detect disposable email addresses at signup. `disban` classifies the domain of an email address as **disposable**, **free** (Gmail, Outlook), **alias** (privacy relays such as SimpleLogin), **testing**, **parked** or **unknown**, and tells you whether it can receive mail. Works without an API key. Standard library only.
4
+
5
+ ```sh
6
+ pip install disban
7
+ ```
8
+
9
+ ```python
10
+ from disban import is_disposable, signup_decision
11
+
12
+ is_disposable("someone@yopmail.com") # True; None means unknown (not verified safe)
13
+
14
+ decision = signup_decision(email)
15
+ if decision["action"] == "block":
16
+ raise ValueError("Please use a permanent email address.")
17
+ if decision["action"] == "fix":
18
+ raise ValueError("That domain can't receive email. Check for a typo.")
19
+ # "allow": create the account; if decision["hold"], hold free credits until the email is confirmed
20
+ ```
21
+
22
+ `signup_decision` fails open: if Disban is slow, unreachable or rate-limited, it allows the signup with `hold: True`.
23
+
24
+ ## FastAPI
25
+
26
+ ```python
27
+ from fastapi import FastAPI, HTTPException
28
+ from disban import signup_decision
29
+
30
+ app = FastAPI()
31
+
32
+ @app.post("/signup")
33
+ def signup(email: str):
34
+ decision = signup_decision(email)
35
+ if decision["action"] != "allow":
36
+ raise HTTPException(422, decision["reason"])
37
+ return {"ok": True, "review": decision.get("hold", False)}
38
+ ```
39
+
40
+ ## Limits and API keys
41
+
42
+ Without a key: 10 checks an hour and 30 a day per network, from your server. For production, set `DISBAN_API_KEY`: a free key includes **2,000 checks a month** with the evidence behind each result. [disban.io/signup](https://disban.io/signup)
43
+
44
+ ```sh
45
+ disban check someone@mailinator.com example.com
46
+ ```
47
+
48
+ [Docs](https://disban.io/docs) · [Methodology](https://disban.io/methodology) · [llms.txt](https://disban.io/llms.txt)
@@ -0,0 +1,69 @@
1
+ """Disban: detect disposable email at signup. Standard library only.
2
+
3
+ from disban import is_disposable, signup_decision
4
+ is_disposable("someone@yopmail.com") # True
5
+
6
+ Without an API key this uses the keyless endpoint (10/hour, 30/day per network). Set DISBAN_API_KEY
7
+ (or pass api_key=) for 2,000 free checks a month with full evidence: https://disban.io/signup
8
+ """
9
+ import json
10
+ import os
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+
15
+ __all__ = ["check", "is_disposable", "signup_decision", "DisbanError"]
16
+ __version__ = "0.1.0"
17
+ BASE = "https://disban.io"
18
+
19
+
20
+ class DisbanError(Exception):
21
+ def __init__(self, message, status=None, retry_after=None):
22
+ super().__init__(message)
23
+ self.status = status
24
+ self.retry_after = retry_after
25
+
26
+
27
+ def check(value, api_key=None, timeout=5.0, base_url=BASE):
28
+ """Classify the domain of an email address or a bare domain. Returns the API's JSON as a dict."""
29
+ if not isinstance(value, str) or not value.strip():
30
+ raise ValueError("Pass an email address or domain.")
31
+ api_key = api_key or os.environ.get("DISBAN_API_KEY")
32
+ headers = {"User-Agent": f"disban-python/{__version__}"}
33
+ if api_key:
34
+ headers.update({"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
35
+ req = urllib.request.Request(f"{base_url}/api/check", json.dumps({"input": value.strip()}).encode(), headers)
36
+ else:
37
+ req = urllib.request.Request(f"{base_url}/api/v1/check/{urllib.parse.quote(value.strip(), safe='')}", headers=headers)
38
+ try:
39
+ with urllib.request.urlopen(req, timeout=timeout) as r:
40
+ return json.load(r)
41
+ except urllib.error.HTTPError as e:
42
+ try:
43
+ message = json.load(e).get("error")
44
+ except Exception:
45
+ message = None
46
+ retry = e.headers.get("Retry-After")
47
+ raise DisbanError(message or f"Disban returned HTTP {e.code}", e.code, int(retry) if retry and retry.isdigit() else None) from None
48
+ except (urllib.error.URLError, TimeoutError) as e:
49
+ raise DisbanError(f"Disban unreachable: {e}") from None
50
+
51
+
52
+ def is_disposable(value, **options):
53
+ """True, False, or None when unknown (not verified safe)."""
54
+ return check(value, **options).get("disposable")
55
+
56
+
57
+ def signup_decision(value, **options):
58
+ """block disposable/testing, fix undeliverable domains, allow the rest; fail open (hold=True) on errors."""
59
+ try:
60
+ r = check(value, **options)
61
+ except DisbanError as e:
62
+ return {"action": "allow", "hold": True, "reason": f"disban_{e.status}" if e.status else "disban_unavailable"}
63
+ routing = r.get("mail_routing")
64
+ routing = routing.get("status") if isinstance(routing, dict) else routing
65
+ if routing == "invalid":
66
+ return {"action": "fix", "reason": "no_mail_route", "result": r}
67
+ if r.get("category") in ("disposable", "testing"):
68
+ return {"action": "block", "reason": r["category"], "result": r}
69
+ return {"action": "allow", "hold": r.get("category") == "unknown", "reason": r.get("category"), "result": r}
@@ -0,0 +1,24 @@
1
+ """python -m disban check <email|domain>..."""
2
+ import json
3
+ import sys
4
+
5
+ from . import DisbanError, check
6
+
7
+
8
+ def main():
9
+ args = sys.argv[1:]
10
+ if len(args) < 2 or args[0] != "check":
11
+ print("Usage: disban check <email|domain>...\nSet DISBAN_API_KEY for 2,000 free checks a month: https://disban.io/signup")
12
+ return 1
13
+ failed = False
14
+ for value in args[1:]:
15
+ try:
16
+ print(json.dumps(check(value)))
17
+ except DisbanError as e:
18
+ failed = True
19
+ print(f"{value}: {e}", file=sys.stderr)
20
+ return 1 if failed else 0
21
+
22
+
23
+ if __name__ == "__main__":
24
+ sys.exit(main())
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "disban"
7
+ version = "0.1.0"
8
+ description = "Detect disposable email at signup: classify an email domain as disposable, free, alias, testing, parked or unknown. Works without an API key."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ authors = [{ name = "Disban", email = "info@disban.io" }]
13
+ keywords = ["disposable-email", "temporary-email", "email-validation", "signup", "burner-email"]
14
+ classifiers = ["Programming Language :: Python :: 3", "Topic :: Communications :: Email", "Intended Audience :: Developers"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://disban.io"
18
+ Documentation = "https://disban.io/docs"
19
+ Support = "https://disban.io/support"
20
+
21
+ [project.scripts]
22
+ disban = "disban.__main__:main"
@@ -0,0 +1,30 @@
1
+ import io, json, unittest, urllib.error
2
+ from unittest import mock
3
+ import disban
4
+
5
+
6
+ def response(body):
7
+ return mock.MagicMock(__enter__=lambda s: io.BytesIO(json.dumps(body).encode()), __exit__=lambda *a: False)
8
+
9
+
10
+ class Test(unittest.TestCase):
11
+ def test_keyless_and_keyed_urls(self):
12
+ with mock.patch("urllib.request.urlopen", return_value=response({"disposable": True})) as u:
13
+ self.assertTrue(disban.is_disposable("p@x.com", api_key=None))
14
+ self.assertTrue(u.call_args[0][0].full_url.endswith("/api/v1/check/p%40x.com"))
15
+ with mock.patch("urllib.request.urlopen", return_value=response({"category": "free"})) as u:
16
+ disban.check("p@x.com", api_key="k")
17
+ self.assertEqual(u.call_args[0][0].get_header("Authorization"), "Bearer k")
18
+
19
+ def test_fail_open_and_decisions(self):
20
+ err = urllib.error.HTTPError("u", 429, "x", {"Retry-After": "60"}, io.BytesIO(b'{"error":"limit"}'))
21
+ with mock.patch("urllib.request.urlopen", side_effect=err):
22
+ self.assertEqual(disban.signup_decision("x.com", api_key=None), {"action": "allow", "hold": True, "reason": "disban_429"})
23
+ with mock.patch("urllib.request.urlopen", return_value=response({"category": "unknown", "mail_routing": "invalid"})):
24
+ self.assertEqual(disban.signup_decision("a@gmial.com", api_key=None)["action"], "fix")
25
+ with mock.patch("urllib.request.urlopen", return_value=response({"category": "disposable", "mail_routing": "routable"})):
26
+ self.assertEqual(disban.signup_decision("a@yopmail.com", api_key=None)["action"], "block")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ unittest.main()