temp-email-filter 0.1.1__py3-none-any.whl

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
+
@@ -0,0 +1,3 @@
1
+ from .checker import check_email, is_disposable_email
2
+
3
+ __all__ = ["check_email", "is_disposable_email"]
@@ -0,0 +1,266 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ import socket
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Optional, Tuple, cast
8
+
9
+ import diskcache
10
+ import dns.resolver
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Configure DNS timeouts to prevent hanging
15
+ try:
16
+ dns.resolver.default_resolver.timeout = 2
17
+ dns.resolver.default_resolver.lifetime = 5
18
+ except (AttributeError, TypeError):
19
+ # Some environments may not support resolver configuration
20
+ pass
21
+
22
+ DEFAULT_CACHE_DIR = Path(os.getenv("TEMP_EMAIL_FILTER_CACHE_DIR", ".email_cache"))
23
+ cache = diskcache.Cache(str(DEFAULT_CACHE_DIR))
24
+
25
+ KNOWN_NOT_DISPOSABLE_DOMAINS = {
26
+ "gmail.com",
27
+ "yahoo.com",
28
+ "hotmail.com",
29
+ "outlook.com",
30
+ "protonmail.com",
31
+ "aol.com",
32
+ "icloud.com",
33
+ "zoho.com",
34
+ "mail.com",
35
+ "gmx.com",
36
+ }
37
+
38
+ DISPOSABLE_DOMAINS = {
39
+ "tempmail.com",
40
+ "throwawaymail.com",
41
+ "10minutemail.com",
42
+ "guerrillamail.com",
43
+ "mailinator.com",
44
+ "sharklasers.com",
45
+ "meltmail.com",
46
+ "yopmail.com",
47
+ "fakeinbox.com",
48
+ "trashmail.com",
49
+ "mintemail.com",
50
+ "maildrop.cc",
51
+ "getnada.com",
52
+ "dispostable.com",
53
+ "spamgourmet.com",
54
+ "jetable.org",
55
+ "mailnesia.com",
56
+ "spamavert.com",
57
+ "emailondeck.com",
58
+ "mytemp.email",
59
+ "tmail.com",
60
+ "boun.cr",
61
+ "mailcatch.com",
62
+ "temp-mail.org",
63
+ "moakt.com",
64
+ "dropmail.me",
65
+ "burnermail.io",
66
+ "mailinator.net",
67
+ "trashmail.net",
68
+ "20minutemail.com",
69
+ "spambog.com",
70
+ "fake-mail.net",
71
+ "emailtemporario.com.br",
72
+ "fakemailgenerator.com",
73
+ "getairmail.com",
74
+ "tempail.com",
75
+ "anonymbox.com",
76
+ "luxusmail.org",
77
+ "eyepaste.com",
78
+ "instant-email.org",
79
+ "easytrashmail.com",
80
+ "dockstones.com",
81
+ }
82
+
83
+ DISPOSABLE_PATTERNS = (
84
+ # Only keep the numeric timestamp pattern - remove the + pattern which flags legitimate aliases
85
+ re.compile(r"^[a-zA-Z0-9]+\.[0-9]{10}@"),
86
+ )
87
+
88
+ RBL_SERVICES = (
89
+ "multi.surbl.org",
90
+ "zen.spamhaus.org",
91
+ "b.barracudacentral.org",
92
+ "bl.spamcop.net",
93
+ "dnsbl.sorbs.net",
94
+ "dnsbl.httpbl.org",
95
+ )
96
+
97
+ BLOCKED_MX_RECORDS = {
98
+ "mx2.den.yt",
99
+ }
100
+
101
+ EMAIL_RE = re.compile(r"^[^@\s]{1,64}@[^@\s]{1,253}$")
102
+ CACHE_TTL_SECONDS = 60 * 60 * 24
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class EmailCheckResult:
107
+ email: str
108
+ is_disposable: bool
109
+ reason: str
110
+
111
+
112
+ def _normalize_email(email: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
113
+ normalized = email.strip().lower()
114
+ if not normalized or len(normalized) > 254 or not EMAIL_RE.match(normalized):
115
+ return None, None, "Invalid email format"
116
+
117
+ local_part, domain = normalized.rsplit("@", 1)
118
+ if not local_part or not domain or ".." in domain:
119
+ return None, None, "Invalid email format"
120
+
121
+ try:
122
+ ascii_domain = domain.encode("idna").decode("ascii")
123
+ except UnicodeError:
124
+ return None, None, "Invalid email domain"
125
+
126
+ return f"{local_part}@{ascii_domain}", ascii_domain, None
127
+
128
+
129
+ def is_valid_domain(domain: str) -> Tuple[bool, str]:
130
+ try:
131
+ mx_records = dns.resolver.resolve(domain, "MX")
132
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
133
+ return False, "Invalid domain or no MX records"
134
+ except Exception:
135
+ logger.exception("Error checking domain MX records")
136
+ return False, "Error checking domain MX records"
137
+
138
+ for mx in mx_records:
139
+ mx_host = str(mx.exchange).rstrip(".").lower()
140
+ if mx_host in BLOCKED_MX_RECORDS:
141
+ return False, f"Blocked MX record: {mx_host}"
142
+
143
+ return True, "Valid MX record"
144
+
145
+
146
+ def check_rbl_ip(ip: str, rbl: str) -> bool:
147
+ """Check if IP address is listed in RBL service."""
148
+ try:
149
+ # Reverse IP octets for RBL query format
150
+ reversed_ip = ".".join(reversed(ip.split(".")))
151
+ query = f"{reversed_ip}.{rbl}"
152
+ dns.resolver.resolve(query, "A")
153
+ return True
154
+ except dns.resolver.NXDOMAIN:
155
+ return False
156
+ except Exception:
157
+ logger.exception("Error checking RBL for IP %s", ip)
158
+ return False
159
+
160
+
161
+ def check_rbl(domain: str) -> Tuple[bool, str]:
162
+ """Check if domain's MX IPs are listed in any RBL service."""
163
+ try:
164
+ # Get MX records first
165
+ mx_records = dns.resolver.resolve(domain, "MX")
166
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
167
+ return False, "Cannot resolve MX records for RBL check"
168
+ except Exception:
169
+ logger.exception("Error resolving MX records for RBL check")
170
+ return False, "Error resolving MX records for RBL check"
171
+
172
+ # Check each MX record's IP against RBLs
173
+ for mx in mx_records:
174
+ mx_host = str(mx.exchange).rstrip(".").lower()
175
+
176
+ try:
177
+ # Resolve MX hostname to IP
178
+ a_records = dns.resolver.resolve(mx_host, "A")
179
+ for a_record in a_records:
180
+ ip = str(a_record)
181
+
182
+ # Check this IP against all RBL services
183
+ for rbl in RBL_SERVICES:
184
+ if check_rbl_ip(ip, rbl):
185
+ return True, f"MX IP {ip} listed in {rbl}"
186
+
187
+ except Exception:
188
+ logger.exception("Error resolving MX hostname %s to IP", mx_host)
189
+ continue
190
+
191
+ return False, "Not listed in any RBL"
192
+
193
+
194
+ def cache_result(domain: str, is_disposable: bool, reason: str) -> None:
195
+ try:
196
+ cache.set(
197
+ domain,
198
+ {"is_disposable": is_disposable, "reason": reason},
199
+ expire=CACHE_TTL_SECONDS,
200
+ )
201
+ except Exception:
202
+ logger.exception("Error caching domain result")
203
+
204
+
205
+ def is_disposable_email(email: str) -> Tuple[bool, str]:
206
+ result = check_email(email)
207
+ return result.is_disposable, result.reason
208
+
209
+
210
+ def check_email(email: str) -> EmailCheckResult:
211
+ normalized_email, domain, validation_error = _normalize_email(email)
212
+ if validation_error or not normalized_email or not domain:
213
+ return EmailCheckResult(
214
+ email=email,
215
+ is_disposable=True,
216
+ reason=validation_error or "Invalid email format",
217
+ )
218
+
219
+ # Check known trusted providers first - skip all other checks
220
+ if domain in KNOWN_NOT_DISPOSABLE_DOMAINS:
221
+ return EmailCheckResult(
222
+ email=normalized_email,
223
+ is_disposable=False,
224
+ reason="Known trusted provider",
225
+ )
226
+
227
+ # Check cache after trusted provider check
228
+ cached_result = cast(Any, cache.get(domain))
229
+ if cached_result:
230
+ return EmailCheckResult(
231
+ email=normalized_email,
232
+ is_disposable=bool(cached_result["is_disposable"]),
233
+ reason=str(cached_result["reason"]),
234
+ )
235
+
236
+ # Check if domain has valid MX records and is not using blocked MX hosts
237
+ is_valid, mx_reason = is_valid_domain(domain)
238
+ if not is_valid:
239
+ # Only cache permanent failures, not transient DNS errors
240
+ if not mx_reason.startswith("Error"):
241
+ cache_result(domain, True, mx_reason)
242
+ return EmailCheckResult(normalized_email, True, mx_reason)
243
+
244
+ # Check against known disposable domains list
245
+ if domain in DISPOSABLE_DOMAINS:
246
+ reason = "Known disposable domain"
247
+ cache_result(domain, True, reason)
248
+ return EmailCheckResult(normalized_email, True, reason)
249
+
250
+ # Check against disposable patterns
251
+ for pattern in DISPOSABLE_PATTERNS:
252
+ if pattern.match(normalized_email):
253
+ reason = "Matched disposable pattern"
254
+ cache_result(domain, True, reason)
255
+ return EmailCheckResult(normalized_email, True, reason)
256
+
257
+ # Check against RBLs (now properly checks MX IPs)
258
+ is_listed, rbl_reason = check_rbl(domain)
259
+ if is_listed:
260
+ cache_result(domain, True, rbl_reason)
261
+ return EmailCheckResult(normalized_email, True, rbl_reason)
262
+
263
+ # Domain appears legitimate - cache this result too
264
+ reason = "Not disposable"
265
+ cache_result(domain, False, reason)
266
+ return EmailCheckResult(normalized_email, False, reason)
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: temp-email-filter
3
+ Version: 0.1.1
4
+ Summary: Detect disposable email addresses with domain, MX, pattern, and RBL checks.
5
+ Author: Temp Email Filter contributors
6
+ License-Expression: MIT
7
+ Keywords: email,disposable-email,validation,dns,temporary-email
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Communications :: Email
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: diskcache>=5.6
20
+ Requires-Dist: dnspython>=2.6
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # Temp Email Filter
26
+
27
+ Temp Email Filter is a Python package for detecting disposable email addresses. It performs comprehensive checks including known disposable domains, trusted provider validation, email patterns, MX record validation, blocked MX hosts, and DNS-based RBL (Real-time Blackhole List) services.
28
+
29
+ ## Features
30
+
31
+ - **Trusted provider check**: Immediately validates emails from known legitimate providers (Gmail, Yahoo, etc.)
32
+ - **Disposable domain detection**: Checks against known temporary email services
33
+ - **Pattern analysis**: Identifies suspicious email patterns (excludes legitimate + aliases)
34
+ - **MX record validation**: Verifies domain has valid mail servers
35
+ - **RBL checking**: Queries reputation databases using proper IP-based lookups
36
+ - **Intelligent caching**: Caches both positive and negative results for performance
37
+ - **DNS timeouts**: Prevents hanging on slow DNS queries
38
+ - **Input validation**: Normalizes and validates email format before processing
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install temp-email-filter
44
+ ```
45
+
46
+ For local development from a clone:
47
+
48
+ ```bash
49
+ git clone <your-repo-url>
50
+ cd temp_email_filter
51
+ python -m venv .venv
52
+ source .venv/bin/activate
53
+ pip install -e ".[dev]"
54
+ ```
55
+
56
+ ## Basic Usage
57
+
58
+ ```python
59
+ from temp_email_filter import check_email
60
+
61
+ result = check_email("test@gmail.com")
62
+ print(f"Email: {result.email}")
63
+ print(f"Is disposable: {result.is_disposable}")
64
+ print(f"Reason: {result.reason}")
65
+
66
+ if result.is_disposable:
67
+ print("This email should be rejected")
68
+ else:
69
+ print("This email is acceptable")
70
+ ```
71
+
72
+ If you only need the original tuple style:
73
+
74
+ ```python
75
+ from temp_email_filter import is_disposable_email
76
+
77
+ is_disposable, reason = is_disposable_email("test@gmail.com")
78
+ ```
79
+
80
+ ## Django Example
81
+
82
+ ```python
83
+ from django.core.exceptions import ValidationError
84
+ from temp_email_filter import check_email
85
+
86
+
87
+ def validate_non_disposable_email(value):
88
+ result = check_email(value)
89
+ if result.is_disposable:
90
+ raise ValidationError(result.reason)
91
+ ```
92
+
93
+ Use the validator in a model or serializer field.
94
+
95
+ ## FastAPI Example
96
+
97
+ This package does not start a FastAPI server. Add it to your own FastAPI app:
98
+
99
+ ```python
100
+ from fastapi import FastAPI, HTTPException
101
+ from temp_email_filter import check_email
102
+
103
+ app = FastAPI()
104
+
105
+
106
+ @app.get("/email-checker")
107
+ def email_checker(email: str):
108
+ result = check_email(email)
109
+ if result.reason.startswith("Invalid email"):
110
+ raise HTTPException(status_code=400, detail=result.reason)
111
+
112
+ return {
113
+ "email": result.email,
114
+ "is_disposable": result.is_disposable,
115
+ "reason": result.reason,
116
+ }
117
+ ```
118
+
119
+ ## Running As A Service
120
+
121
+ This project is package-only. To run it as a service, install it inside your own Django, FastAPI, Flask, worker, or microservice project and call `check_email()` from your route, form validation, serializer, or background job.
122
+
123
+ ## Configuration
124
+
125
+ The cache directory defaults to `.email_cache`. Override it with:
126
+
127
+ ```bash
128
+ export TEMP_EMAIL_FILTER_CACHE_DIR=/tmp/temp-email-filter-cache
129
+ ```
130
+
131
+ ## Recent Improvements (v0.1.1)
132
+
133
+ - **Fixed RBL checking**: Now properly queries MX record IPs instead of domains
134
+ - **Improved pattern matching**: Removed false positive for legitimate + aliases (john+work@gmail.com)
135
+ - **Enhanced caching**: Now caches both disposable and legitimate results
136
+ - **Better logic flow**: Trusted providers checked first, bypassing expensive DNS operations
137
+ - **DNS timeouts**: Added configurable timeouts to prevent hanging queries
138
+ - **Transient error handling**: Avoids caching temporary DNS failures
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ pip install -e ".[dev]"
144
+ pytest
145
+ ```
146
+
147
+ ## Publishing
148
+
149
+ Maintainer release steps are documented in [`docs/PYPI.md`](docs/PYPI.md).
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,8 @@
1
+ email_span_filter/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ temp_email_filter/__init__.py,sha256=QUKIvs46DfihRdYrPFnbxjyVXDHOvRLT5mDKDmWlcpg,103
3
+ temp_email_filter/checker.py,sha256=xf2pxS3zfphGriOuplibQ3haWof02yThBv_Li_RdSGk,8024
4
+ temp_email_filter-0.1.1.dist-info/licenses/LICENSE,sha256=fSuk3AWQxAFoSSLUtbuph4BEuheUj3Nu153argFxBkE,1087
5
+ temp_email_filter-0.1.1.dist-info/METADATA,sha256=-1YaTt5DQWx024CXclF4u44b_hNHs2idjvHHqHsVq34,4599
6
+ temp_email_filter-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ temp_email_filter-0.1.1.dist-info/top_level.txt,sha256=Oc_XlWKwU8BRwfhkDXcmHzthT-HjZRt7WxKPfuIUJmM,36
8
+ temp_email_filter-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Temp Email Filter contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ email_span_filter
2
+ temp_email_filter