Skip to content

Commit 70e41c1

Browse files
committed
feat: add setEmailForwarding, getContacts
- dns.set_email_forwarding() to set/replace email forwarding rules - domains.get_contacts() returning DomainContacts model (registrant, tech, admin, billing) - CLI: dns set-email-forwarding, domain contacts - Bump to 1.3.0
1 parent 3de8437 commit 70e41c1

File tree

8 files changed

+216
-8
lines changed

8 files changed

+216
-8
lines changed

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,24 @@ print(bal.funds_required_for_auto_renew) # Decimal('20.16')
338338
### Email Forwarding
339339

340340
```python
341+
# Read
341342
rules = nc.dns.get_email_forwarding("example.com")
342343
for r in rules:
343344
print(f"{r.mailbox} -> {r.forward_to}")
345+
346+
# Write (replaces all existing rules)
347+
nc.dns.set_email_forwarding("example.com", [
348+
EmailForward(mailbox="info", forward_to="me@gmail.com"),
349+
EmailForward(mailbox="support", forward_to="help@gmail.com"),
350+
])
351+
```
352+
353+
### Domain Contacts
354+
355+
```python
356+
contacts = nc.domains.get_contacts("example.com")
357+
print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
358+
print(contacts.registrant.email)
344359
```
345360
346361
### Domain Management
@@ -420,11 +435,10 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
420435
421436
| API | Status | Methods |
422437
|-----|--------|---------|
423-
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
424-
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding` |
438+
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
439+
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
425440
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
426-
| `namecheap.domains.*` | 🚧 Planned | `getContacts`, `getTldList`, `reactivate` |
427-
| `namecheap.domains.dns.*` | 🚧 Planned | `setEmailForwarding` |
441+
| `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
428442
| `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
429443
| `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
430444
| `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "namecheap-python"
3-
version = "1.2.0"
3+
version = "1.3.0"
44
description = "A friendly Python SDK for Namecheap API"
55
authors = [{name = "Adrian Galilea Delgado", email = "adriangalilea@gmail.com"}]
66
readme = "README.md"

src/namecheap/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,21 @@
1818
DNSRecord,
1919
Domain,
2020
DomainCheck,
21+
DomainContacts,
2122
DomainInfo,
2223
EmailForward,
2324
Nameservers,
2425
)
2526

26-
__version__ = "1.2.0"
27+
__version__ = "1.3.0"
2728
__all__ = [
2829
"AccountBalance",
2930
"ConfigurationError",
3031
"Contact",
3132
"DNSRecord",
3233
"Domain",
3334
"DomainCheck",
35+
"DomainContacts",
3436
"DomainInfo",
3537
"EmailForward",
3638
"Namecheap",

src/namecheap/_api/dns.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,41 @@ def get_email_forwarding(self, domain: str) -> list[EmailForward]:
528528
EmailForward(mailbox=f.get("@mailbox", ""), forward_to=f.get("#text", ""))
529529
for f in forwards
530530
]
531+
532+
def set_email_forwarding(
533+
self, domain: str, rules: list[EmailForward] | list[dict[str, str]]
534+
) -> bool:
535+
"""
536+
Set email forwarding rules for a domain. Replaces all existing rules.
537+
538+
Args:
539+
domain: Domain name
540+
rules: List of EmailForward or dicts with 'mailbox' and 'forward_to' keys
541+
542+
Returns:
543+
True if successful
544+
545+
Examples:
546+
>>> nc.dns.set_email_forwarding("example.com", [
547+
... EmailForward(mailbox="info", forward_to="me@gmail.com"),
548+
... EmailForward(mailbox="support", forward_to="help@gmail.com"),
549+
... ])
550+
"""
551+
assert rules, "At least one forwarding rule is required"
552+
553+
params: dict[str, Any] = {"DomainName": domain}
554+
for i, rule in enumerate(rules, 1):
555+
if isinstance(rule, EmailForward):
556+
params[f"MailBox{i}"] = rule.mailbox
557+
params[f"ForwardTo{i}"] = rule.forward_to
558+
else:
559+
params[f"MailBox{i}"] = rule["mailbox"]
560+
params[f"ForwardTo{i}"] = rule["forward_to"]
561+
562+
result: Any = self._request(
563+
"namecheap.domains.dns.setEmailForwarding",
564+
params,
565+
path="DomainDNSSetEmailForwardingResult",
566+
)
567+
568+
return bool(result and result.get("@IsSuccess", "false").lower() == "true")

src/namecheap/_api/domains.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import tldextract
1010

1111
from namecheap.logging import logger
12-
from namecheap.models import Contact, Domain, DomainCheck, DomainInfo
12+
from namecheap.models import Contact, Domain, DomainCheck, DomainContacts, DomainInfo
1313

1414
from .base import BaseAPI
1515

@@ -156,6 +156,52 @@ def get_info(self, domain: str) -> DomainInfo:
156156

157157
return DomainInfo.model_validate(flat)
158158

159+
def get_contacts(self, domain: str) -> DomainContacts:
160+
"""
161+
Get contact information for a domain.
162+
163+
Args:
164+
domain: Domain name
165+
166+
Returns:
167+
DomainContacts with registrant, tech, admin, and aux_billing contacts
168+
169+
Examples:
170+
>>> contacts = nc.domains.get_contacts("example.com")
171+
>>> print(contacts.registrant.email)
172+
"""
173+
result: Any = self._request(
174+
"namecheap.domains.getContacts",
175+
{"DomainName": domain},
176+
path="DomainContactsResult",
177+
)
178+
179+
assert result, f"API returned empty result for {domain} getContacts"
180+
181+
def parse_contact(data: dict[str, Any]) -> Contact:
182+
return Contact.model_validate(
183+
{
184+
"FirstName": data.get("FirstName", ""),
185+
"LastName": data.get("LastName", ""),
186+
"Organization": data.get("Organization"),
187+
"Address1": data.get("Address1", ""),
188+
"Address2": data.get("Address2"),
189+
"City": data.get("City", ""),
190+
"StateProvince": data.get("StateProvince", ""),
191+
"PostalCode": data.get("PostalCode", ""),
192+
"Country": data.get("Country", ""),
193+
"Phone": data.get("Phone", ""),
194+
"EmailAddress": data.get("EmailAddress", ""),
195+
}
196+
)
197+
198+
return DomainContacts(
199+
registrant=parse_contact(result.get("Registrant", {})),
200+
tech=parse_contact(result.get("Tech", {})),
201+
admin=parse_contact(result.get("Admin", {})),
202+
aux_billing=parse_contact(result.get("AuxBilling", {})),
203+
)
204+
159205
def register(
160206
self,
161207
domain: str,

src/namecheap/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,17 @@ class Contact(BaseModel):
336336
model_config = ConfigDict(populate_by_name=True)
337337

338338

339+
class DomainContacts(BaseModel):
340+
"""Contact information for all roles on a domain."""
341+
342+
registrant: Contact
343+
tech: Contact
344+
admin: Contact
345+
aux_billing: Contact
346+
347+
model_config = ConfigDict(populate_by_name=True)
348+
349+
339350
class Config(BaseModel):
340351
"""Client configuration with validation."""
341352

src/namecheap_cli/__main__.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,103 @@ def dns_email_forwarding(config: Config, domain: str) -> None:
919919
sys.exit(1)
920920

921921

922+
@dns_group.command("set-email-forwarding")
923+
@click.argument("domain")
924+
@click.argument("rules", nargs=-1, required=True)
925+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
926+
@pass_config
927+
def dns_set_email_forwarding(
928+
config: Config, domain: str, rules: tuple[str, ...], yes: bool
929+
) -> None:
930+
"""Set email forwarding rules. Replaces all existing rules.
931+
932+
Rules are in mailbox:forward_to format.
933+
934+
Example:
935+
namecheap-cli dns set-email-forwarding example.com info:me@gmail.com support:help@gmail.com
936+
"""
937+
nc = config.init_client()
938+
939+
parsed = []
940+
for rule in rules:
941+
assert ":" in rule, f"Invalid rule format '{rule}', expected mailbox:forward_to"
942+
mailbox, forward_to = rule.split(":", 1)
943+
parsed.append({"mailbox": mailbox, "forward_to": forward_to})
944+
945+
try:
946+
if not yes and not config.quiet:
947+
console.print(f"\n[yellow]Setting email forwarding for {domain}:[/yellow]")
948+
for p in parsed:
949+
console.print(f" • {p['mailbox']}@{domain}{p['forward_to']}")
950+
console.print()
951+
952+
if not Confirm.ask("Continue?", default=True):
953+
console.print("[yellow]Cancelled[/yellow]")
954+
return
955+
956+
with Progress(
957+
SpinnerColumn(),
958+
TextColumn("[progress.description]{task.description}"),
959+
transient=True,
960+
) as progress:
961+
progress.add_task(f"Setting email forwarding for {domain}...", total=None)
962+
success = nc.dns.set_email_forwarding(domain, parsed)
963+
964+
if success:
965+
console.print("[green]✅ Email forwarding updated successfully![/green]")
966+
else:
967+
console.print("[red]❌ Failed to update email forwarding[/red]")
968+
sys.exit(1)
969+
970+
except NamecheapError as e:
971+
console.print(f"[red]❌ Error: {e}[/red]")
972+
sys.exit(1)
973+
974+
975+
@domain_group.command("contacts")
976+
@click.argument("domain")
977+
@pass_config
978+
def domain_contacts(config: Config, domain: str) -> None:
979+
"""Show contact information for a domain."""
980+
nc = config.init_client()
981+
982+
try:
983+
with Progress(
984+
SpinnerColumn(),
985+
TextColumn("[progress.description]{task.description}"),
986+
transient=True,
987+
) as progress:
988+
progress.add_task(f"Getting contacts for {domain}...", total=None)
989+
contacts = nc.domains.get_contacts(domain)
990+
991+
if config.output_format == "table":
992+
for role, contact in [
993+
("Registrant", contacts.registrant),
994+
("Tech", contacts.tech),
995+
("Admin", contacts.admin),
996+
("Billing", contacts.aux_billing),
997+
]:
998+
console.print(f"\n[bold cyan]{role}[/bold cyan]")
999+
console.print(f" {contact.first_name} {contact.last_name}")
1000+
if contact.organization:
1001+
console.print(f" {contact.organization}")
1002+
console.print(f" {contact.email}")
1003+
console.print(f" {contact.phone}")
1004+
console.print(f" {contact.address1}")
1005+
if contact.address2:
1006+
console.print(f" {contact.address2}")
1007+
console.print(
1008+
f" {contact.city}, {contact.state_province} {contact.postal_code}"
1009+
)
1010+
console.print(f" {contact.country}")
1011+
else:
1012+
output_formatter(contacts.model_dump(), config.output_format)
1013+
1014+
except NamecheapError as e:
1015+
console.print(f"[red]❌ Error: {e}[/red]")
1016+
sys.exit(1)
1017+
1018+
9221019
@cli.group("account")
9231020
def account_group() -> None:
9241021
"""Account management commands."""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)