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.
- disban-0.1.0/.gitignore +39 -0
- disban-0.1.0/PKG-INFO +64 -0
- disban-0.1.0/README.md +48 -0
- disban-0.1.0/disban/__init__.py +69 -0
- disban-0.1.0/disban/__main__.py +24 -0
- disban-0.1.0/pyproject.toml +22 -0
- disban-0.1.0/test_disban.py +30 -0
disban-0.1.0/.gitignore
ADDED
|
@@ -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()
|