Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 24 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ from truelist import Truelist
client = Truelist("your-api-key")

result = client.email.validate("user@example.com")
print(result.state) # "valid"
print(result.sub_state) # "ok"
print(result.free_email) # True
print(result.state) # "ok"
print(result.sub_state) # "email_ok"
print(result.domain) # "example.com"
print(result.is_valid) # True
```

Expand Down Expand Up @@ -120,20 +120,16 @@ Set `max_retries=0` to disable retries.
### Email Validation

```python
# Server-side validation
result = client.email.validate("user@example.com")

# Form/frontend validation (different rate limits)
result = client.email.form_validate("user@example.com")
```

### Account Info

```python
account = client.account.get()
print(account.email) # "you@company.com"
print(account.plan) # "pro"
print(account.credits) # 9500
print(account.email) # "you@company.com"
print(account.name) # "Your Name"
print(account.payment_plan) # "pro"
```

## Types Reference
Expand All @@ -143,37 +139,38 @@ print(account.credits) # 9500
| Field | Type | Description |
|-------|------|-------------|
| `email` | `str` | The email address that was validated |
| `state` | `str` | One of: `valid`, `invalid`, `risky`, `unknown` |
| `domain` | `str` | The domain of the email address |
| `canonical` | `str \| None` | The canonical (local) part of the email |
| `mx_record` | `str \| None` | The MX record for the domain |
| `first_name` | `str \| None` | First name associated with the email, if available |
| `last_name` | `str \| None` | Last name associated with the email, if available |
| `state` | `str` | One of: `ok`, `email_invalid`, `risky`, `unknown`, `accept_all` |
| `sub_state` | `str` | Detailed reason (see below) |
| `free_email` | `bool` | Whether the email is from a free provider |
| `role` | `bool` | Whether the email is a role address (e.g., info@) |
| `disposable` | `bool` | Whether the email is from a disposable provider |
| `verified_at` | `str \| None` | Timestamp of when the verification was performed |
| `suggestion` | `str \| None` | Suggested correction, if any |

Convenience properties: `is_valid`, `is_invalid`, `is_risky`, `is_unknown`
Convenience properties: `is_valid`, `is_invalid`, `is_risky`, `is_unknown`, `is_disposable`, `is_role`

#### Sub-states

| Sub-state | Description |
|-----------|-------------|
| `ok` | Email is valid |
| `accept_all` | Domain accepts all emails (risky) |
| `disposable_address` | Disposable email provider |
| `role_address` | Role-based address (info@, admin@, etc.) |
| `failed_mx_check` | Domain has no MX records |
| `failed_spam_trap` | Known spam trap |
| `failed_no_mailbox` | Mailbox does not exist |
| `failed_greylisted` | Server temporarily rejected (greylisting) |
| `failed_syntax_check` | Email syntax is invalid |
| `unknown` | Could not determine status |
| `email_ok` | Email is valid |
| `is_disposable` | Disposable email provider |
| `is_role` | Role-based address (info@, admin@, etc.) |
| `unknown_error` | Could not determine status |
| `failed_smtp_check` | SMTP check failed |

### `AccountInfo`

| Field | Type | Description |
|-------|------|-------------|
| `email` | `str` | Account email address |
| `plan` | `str` | Current plan name |
| `credits` | `int` | Remaining validation credits |
| `name` | `str` | Account holder name |
| `uuid` | `str` | Account UUID |
| `time_zone` | `str \| None` | Account time zone |
| `is_admin_role` | `bool` | Whether the user has admin role |
| `payment_plan` | `str` | Current payment plan |

### Exceptions

Expand Down
6 changes: 4 additions & 2 deletions src/truelist/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ def sync_request(
*,
max_retries: int,
json: dict[str, Any] | None = None,
params: dict[str, str] | None = None,
) -> httpx.Response:
last_exc: Exception | None = None

for attempt in range(max_retries + 1):
try:
response = client.request(method, url, json=json)
response = client.request(method, url, json=json, params=params)
except httpx.ConnectError as exc:
last_exc = exc
if attempt < max_retries:
Expand Down Expand Up @@ -140,14 +141,15 @@ async def async_request(
*,
max_retries: int,
json: dict[str, Any] | None = None,
params: dict[str, str] | None = None,
) -> httpx.Response:
import asyncio

last_exc: Exception | None = None

for attempt in range(max_retries + 1):
try:
response = await client.request(method, url, json=json)
response = await client.request(method, url, json=json, params=params)
except httpx.ConnectError as exc:
last_exc = exc
if attempt < max_retries:
Expand Down
56 changes: 22 additions & 34 deletions src/truelist/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, client: httpx.AsyncClient, max_retries: int) -> None:
self._max_retries = max_retries

async def validate(self, email: str) -> ValidationResult:
"""Validate an email address using server-side verification.
"""Validate an email address.

Args:
email: The email address to validate.
Expand All @@ -33,29 +33,9 @@ async def validate(self, email: str) -> ValidationResult:
response = await async_request(
self._client,
"POST",
"/api/v1/verify",
"/api/v1/verify_inline",
max_retries=self._max_retries,
json={"email": email},
)
return _parse_validation_result(response.json())

async def form_validate(self, email: str) -> ValidationResult:
"""Validate an email address using form/frontend verification.

This endpoint has different rate limits suited for frontend form validation.

Args:
email: The email address to validate.

Returns:
A ValidationResult with the verification details.
"""
response = await async_request(
self._client,
"POST",
"/api/v1/form_verify",
max_retries=self._max_retries,
json={"email": email},
params={"email": email},
)
return _parse_validation_result(response.json())

Expand All @@ -71,19 +51,22 @@ async def get(self) -> AccountInfo:
"""Get account information for the authenticated user.

Returns:
An AccountInfo with email, plan, and credits.
An AccountInfo with account details.
"""
response = await async_request(
self._client,
"GET",
"/api/v1/account",
"/me",
max_retries=self._max_retries,
)
data: dict[str, Any] = response.json()
return AccountInfo(
email=data["email"],
plan=data["plan"],
credits=data["credits"],
name=data["name"],
uuid=data["uuid"],
time_zone=data.get("time_zone"),
is_admin_role=data.get("is_admin_role", False),
payment_plan=data["account"]["payment_plan"],
)


Expand Down Expand Up @@ -144,12 +127,17 @@ async def __aexit__(self, *args: object) -> None:


def _parse_validation_result(data: dict[str, Any]) -> ValidationResult:
emails = data["emails"]
email_data = emails[0]
return ValidationResult(
email=data["email"],
state=data["state"],
sub_state=data["sub_state"],
free_email=data["free_email"],
role=data["role"],
disposable=data["disposable"],
suggestion=data.get("suggestion"),
email=email_data["address"],
domain=email_data["domain"],
canonical=email_data.get("canonical"),
mx_record=email_data.get("mx_record"),
first_name=email_data.get("first_name"),
last_name=email_data.get("last_name"),
state=email_data["email_state"],
sub_state=email_data["email_sub_state"],
verified_at=email_data.get("verified_at"),
suggestion=email_data.get("did_you_mean"),
)
56 changes: 22 additions & 34 deletions src/truelist/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, client: httpx.Client, max_retries: int) -> None:
self._max_retries = max_retries

def validate(self, email: str) -> ValidationResult:
"""Validate an email address using server-side verification.
"""Validate an email address.

Args:
email: The email address to validate.
Expand All @@ -33,29 +33,9 @@ def validate(self, email: str) -> ValidationResult:
response = sync_request(
self._client,
"POST",
"/api/v1/verify",
"/api/v1/verify_inline",
max_retries=self._max_retries,
json={"email": email},
)
return _parse_validation_result(response.json())

def form_validate(self, email: str) -> ValidationResult:
"""Validate an email address using form/frontend verification.

This endpoint has different rate limits suited for frontend form validation.

Args:
email: The email address to validate.

Returns:
A ValidationResult with the verification details.
"""
response = sync_request(
self._client,
"POST",
"/api/v1/form_verify",
max_retries=self._max_retries,
json={"email": email},
params={"email": email},
)
return _parse_validation_result(response.json())

Expand All @@ -71,19 +51,22 @@ def get(self) -> AccountInfo:
"""Get account information for the authenticated user.

Returns:
An AccountInfo with email, plan, and credits.
An AccountInfo with account details.
"""
response = sync_request(
self._client,
"GET",
"/api/v1/account",
"/me",
max_retries=self._max_retries,
)
data: dict[str, Any] = response.json()
return AccountInfo(
email=data["email"],
plan=data["plan"],
credits=data["credits"],
name=data["name"],
uuid=data["uuid"],
time_zone=data.get("time_zone"),
is_admin_role=data.get("is_admin_role", False),
payment_plan=data["account"]["payment_plan"],
)


Expand Down Expand Up @@ -143,12 +126,17 @@ def __exit__(self, *args: object) -> None:


def _parse_validation_result(data: dict[str, Any]) -> ValidationResult:
emails = data["emails"]
email_data = emails[0]
return ValidationResult(
email=data["email"],
state=data["state"],
sub_state=data["sub_state"],
free_email=data["free_email"],
role=data["role"],
disposable=data["disposable"],
suggestion=data.get("suggestion"),
email=email_data["address"],
domain=email_data["domain"],
canonical=email_data.get("canonical"),
mx_record=email_data.get("mx_record"),
first_name=email_data.get("first_name"),
last_name=email_data.get("last_name"),
state=email_data["email_state"],
sub_state=email_data["email_sub_state"],
verified_at=email_data.get("verified_at"),
suggestion=email_data.get("did_you_mean"),
)
34 changes: 25 additions & 9 deletions src/truelist/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,25 @@ class ValidationResult:
"""Result of an email validation request."""

email: str
domain: str
canonical: str | None
mx_record: str | None
first_name: str | None
last_name: str | None
state: str
sub_state: str
free_email: bool
role: bool
disposable: bool
verified_at: str | None
suggestion: str | None

@property
def is_valid(self) -> bool:
"""Whether the email state is 'valid'."""
return self.state == "valid"
"""Whether the email state is 'ok'."""
return self.state == "ok"

@property
def is_invalid(self) -> bool:
"""Whether the email state is 'invalid'."""
return self.state == "invalid"
"""Whether the email state is 'email_invalid'."""
return self.state == "email_invalid"

@property
def is_risky(self) -> bool:
Expand All @@ -35,11 +38,24 @@ def is_unknown(self) -> bool:
"""Whether the email state is 'unknown'."""
return self.state == "unknown"

@property
def is_disposable(self) -> bool:
"""Whether the email sub_state is 'is_disposable'."""
return self.sub_state == "is_disposable"

@property
def is_role(self) -> bool:
"""Whether the email sub_state is 'is_role'."""
return self.sub_state == "is_role"


@dataclass(frozen=True)
class AccountInfo:
"""Account information for the authenticated user."""

email: str
plan: str
credits: int
name: str
uuid: str
time_zone: str | None
is_admin_role: bool
payment_plan: str
Loading
Loading