Rewrite namecheap skill in Python to address PR review

Replace the Bash namecheap.sh with a stdlib-only Python CLI (namecheap.py)
that resolves all Copilot PR review feedback:

- Cache the public IP once per run instead of per request
- Build API requests in-process via urllib so the API key never appears
  in process argv or shell history
- Broaden multi-part TLD detection (co.uk, com.au, etc.) and document the
  limitation
- Allow setup to update existing stored credentials
- Stop soliciting the API key via chat; use terminal getpass or env vars
- Remove non-portable Bash constructs (inline local, grep -oP)

Also normalize domain casing, preserve MX priority 0, accept
case-insensitive record types, and handle mixed-case email-forwarding
attributes. Update SKILL.md and regenerate the skills README.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno Borges
2026-05-29 15:23:06 -04:00
parent 65c205fb96
commit b839f3e71b
4 changed files with 721 additions and 944 deletions
+23 -24
View File
@@ -17,67 +17,66 @@ Before executing any API commands, verify credentials are configured:
1. **Check for existing config** — look for `~/.namecheap-api`
2. If not configured, guide the user through setup:
a. **Show public IP** — run `curl -s https://api.ipify.org` to display the user's public IP
a. **Show public IP** — run `python3 namecheap.py public-ip` to display the user's public IP
b. **Instruct IP whitelisting** — tell the user to go to https://ap.www.namecheap.com/settings/tools/apiaccess/, enable API (select ON), and whitelist the displayed IP
c. **Collect credentials** — use `ask_user` to get their Namecheap username, then their API key
d. **Save config**write credentials to `~/.namecheap-api` with `chmod 600`
e. **Validate** — run a test API call to confirm access works
c. **Have the user run setup themselves** — ask the user to run `python3 namecheap.py setup` directly **in their own terminal**. The script prompts for the username and reads the API key with a hidden prompt (`getpass`), writes `~/.namecheap-api` with `chmod 600`, and validates the connection. **Never ask the user to paste their API key into the chat, and never log, echo, or display the API key value.** If you cannot run an interactive terminal for the user, instruct them to run `setup` themselves, or to export `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` as environment variables in their own shell — rather than collecting the secret via `ask_user`.
d. **Confirm**once the user reports setup succeeded, proceed with DNS operations.
### DNS Operations
Use the `namecheap.sh` script (bundled in this skill's directory) for all API interactions:
Use the `namecheap.py` script (bundled in this skill's directory) for all API interactions. It requires only Python 3 (standard library only — no `pip install` needed) and works the same on macOS, Linux, and Windows:
```bash
# Show public IP (for setup)
bash namecheap.sh public-ip
python3 namecheap.py public-ip
# Run setup flow
bash namecheap.sh setup
python3 namecheap.py setup
# List domains
bash namecheap.sh domains.getList
python3 namecheap.py domains.getList
# Get nameservers for a domain (shows if using Namecheap DNS or custom)
bash namecheap.sh domains.dns.getList --domain example.com
python3 namecheap.py domains.dns.getList --domain example.com
# Get DNS records for a domain
bash namecheap.sh domains.dns.getHosts --domain example.com
python3 namecheap.py domains.dns.getHosts --domain example.com
# Add a single record (preserves existing records)
bash namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800
python3 namecheap.py dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800
# Remove a single record
bash namecheap.sh dns.removeHost --domain example.com --type A --name www --address 1.2.3.4
python3 namecheap.py dns.removeHost --domain example.com --type A --name www --address 1.2.3.4
# Replace all records from a JSON file
bash namecheap.sh domains.dns.setHosts --domain example.com --hosts records.json
python3 namecheap.py domains.dns.setHosts --domain example.com --hosts records.json
# Switch to Namecheap default DNS
bash namecheap.sh domains.dns.setDefault --domain example.com
python3 namecheap.py domains.dns.setDefault --domain example.com
# Switch to custom nameservers
bash namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com
python3 namecheap.py domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com
# Get email forwarding rules
bash namecheap.sh domains.dns.getEmailForwarding --domain example.com
python3 namecheap.py domains.dns.getEmailForwarding --domain example.com
# Set email forwarding (single rule)
bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com
python3 namecheap.py domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com
# Set email forwarding (from JSON file)
bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json
python3 namecheap.py domains.dns.setEmailForwarding --domain example.com --forwards forwards.json
# Create a child nameserver (glue record)
bash namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4
python3 namecheap.py domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4
# Delete a child nameserver
bash namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com
python3 namecheap.py domains.ns.delete --domain example.com --nameserver ns1.example.com
# Get nameserver info
bash namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com
python3 namecheap.py domains.ns.getInfo --domain example.com --nameserver ns1.example.com
# Update nameserver IP
bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8
python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8
```
## Behavior
@@ -87,7 +86,7 @@ bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.exampl
- **Use `ask_user` to confirm destructive changes.** Before removing records or replacing all records with `setHosts`, confirm with the user.
- **The Namecheap `setHosts` API replaces ALL records.** Never call `domains.dns.setHosts` directly unless you have fetched all existing records first. Use `dns.addHost` and `dns.removeHost` for safe single-record operations — they handle the fetch-modify-write cycle internally.
- **Explain TTL in human terms.** When the user asks about TTL, explain that 1800 = 30 minutes, 3600 = 1 hour, etc.
- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script handles this automatically.
- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script recognizes a built-in list of common second-level suffixes (e.g. `co.uk`, `com.au`, `co.jp`, `com.br`). This list is best-effort and not a full public-suffix database — if a domain with an unlisted multi-part suffix returns a `2019166` ("Domain not found") error, the SLD/TLD split was likely wrong. In that case, confirm the registered domain with the user and report the limitation.
## Credential Storage
@@ -98,7 +97,7 @@ NAMECHEAP_API_USER="username"
NAMECHEAP_API_KEY="api-key-here"
```
This file must have `600` permissions (owner read/write only).
This file must have `600` permissions (owner read/write only). Alternatively, the script reads credentials from the `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` environment variables, which take precedence over the file when both are set.
## Supported Record Types
+697
View File
@@ -0,0 +1,697 @@
#!/usr/bin/env python3
"""Namecheap API CLI wrapper for DNS management.
Uses only the Python standard library (no third-party dependencies). Credentials
are read from ``~/.namecheap-api`` (or env vars) and are never passed on the
command line, so they cannot leak via ``ps``/shell history.
"""
import argparse
import getpass
import json
import os
import re
import stat
import sys
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
API_URL = "https://api.namecheap.com/xml.response"
CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".namecheap-api")
# Known multi-part (second-level) public suffixes. Best-effort list, not a full
# public-suffix database. For unlisted suffixes the domain is split on the last
# dot, which is correct for single-label TLDs (.com, .io, .dev, ...).
MULTI_PART_SUFFIXES = {
"co.uk", "org.uk", "me.uk", "ac.uk", "gov.uk", "net.uk", "ltd.uk", "plc.uk",
"com.au", "net.au", "org.au", "id.au",
"co.nz", "net.nz", "org.nz",
"co.za", "org.za",
"co.jp", "ne.jp", "or.jp", "ac.jp", "go.jp",
"co.kr", "or.kr", "ne.kr",
"com.br", "net.br", "org.br",
"com.cn", "net.cn", "org.cn",
"co.in", "net.in", "org.in",
"com.mx", "org.mx",
"com.sg", "edu.sg",
"com.tr",
"co.il", "org.il",
}
# Cached public IP for the lifetime of the process.
_public_ip = None
# --- Output helpers -------------------------------------------------------
_USE_COLOR = sys.stdout.isatty()
def _c(code, text):
return f"\033[{code}m{text}\033[0m" if _USE_COLOR else text
def err(msg):
print(_c("0;31", "Error:") + " " + msg, file=sys.stderr)
def success(msg):
print(_c("0;32", "\u2713") + " " + msg)
def info(msg):
print(_c("0;36", "\u2139") + " " + msg)
def warn(msg):
print(_c("1;33", "\u26a0") + " " + msg)
class NamecheapError(Exception):
"""Raised when the API returns a Status="ERROR" response."""
# --- Configuration --------------------------------------------------------
def load_config():
"""Return (api_user, api_key), preferring env vars then the config file."""
api_user = os.environ.get("NAMECHEAP_API_USER")
api_key = os.environ.get("NAMECHEAP_API_KEY")
if api_user and api_key:
return api_user, api_key
if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
content = fh.read()
# File uses shell-style KEY="value" lines for backward compatibility.
pattern = re.compile(r'^\s*([A-Z_]+)\s*=\s*"?([^"\n]*)"?\s*$', re.MULTILINE)
values = {m.group(1): m.group(2) for m in pattern.finditer(content)}
api_user = api_user or values.get("NAMECHEAP_API_USER")
api_key = api_key or values.get("NAMECHEAP_API_KEY")
return api_user, api_key
def check_credentials():
api_user, api_key = load_config()
if not api_user or not api_key:
err("Namecheap API credentials not configured.")
print()
print("Run 'python3 namecheap.py setup' to configure your credentials.")
print()
print("You need:")
print(" 1. Your Namecheap username")
print(" 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/")
print(" 3. Your public IP whitelisted in the API settings")
sys.exit(1)
return api_user, api_key
def save_config(api_user, api_key):
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
fh.write(f'NAMECHEAP_API_USER="{api_user}"\n')
fh.write(f'NAMECHEAP_API_KEY="{api_key}"\n')
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 600
# --- Networking -----------------------------------------------------------
def _http_get(url, timeout=15):
req = urllib.request.Request(url, headers={"User-Agent": "namecheap-skill/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (https only)
return resp.read().decode("utf-8", errors="replace")
def get_public_ip():
"""Resolve the public IP once and cache it for subsequent calls."""
global _public_ip
if _public_ip:
return _public_ip
for url in ("https://api.ipify.org", "https://ifconfig.me"):
try:
ip = _http_get(url, timeout=10).strip()
if ip:
_public_ip = ip
return ip
except Exception:
continue
_public_ip = "unknown"
return _public_ip
def _strip_namespaces(root):
for elem in root.iter():
if isinstance(elem.tag, str) and "}" in elem.tag:
elem.tag = elem.tag.split("}", 1)[1]
return root
def _check_error(root):
if (root.attrib.get("Status") or "").upper() == "ERROR":
messages = []
for e in root.iter("Err"):
code = e.attrib.get("Number") or e.attrib.get("Code") or ""
text = (e.text or "").strip()
messages.append(f"[{code}] {text}" if code else text)
raise NamecheapError("; ".join(m for m in messages if m) or "Unknown API error")
def api_request(command, params=None):
"""Issue a Namecheap API GET request and return the parsed (ns-stripped) root.
The API key is encoded into the request URL inside this process; it is never
passed as a command-line argument, so it cannot leak via ``ps`` or shell
history. Values are URL-encoded by ``urllib``.
"""
api_user, api_key = check_credentials()
query = {
"ApiUser": api_user,
"ApiKey": api_key,
"UserName": api_user,
"Command": f"namecheap.{command}",
"ClientIp": get_public_ip(),
}
for key, value in (params or {}).items():
if value is not None and value != "":
query[key] = value
url = f"{API_URL}?{urllib.parse.urlencode(query)}"
body = _http_get(url, timeout=30)
root = _strip_namespaces(ET.fromstring(body))
_check_error(root)
return root
def _attr(root, tag, name, default=""):
for elem in root.iter(tag):
return elem.attrib.get(name, default)
return default
# --- Domain parsing -------------------------------------------------------
def parse_domain(domain):
"""Split a registered domain into (SLD, TLD), handling multi-part TLDs."""
domain = domain.strip().rstrip(".").lower()
labels = domain.split(".")
if len(labels) >= 3 and ".".join(labels[-2:]) in MULTI_PART_SUFFIXES:
tld = ".".join(labels[-2:])
sld = ".".join(labels[:-2])
elif len(labels) >= 2:
tld = labels[-1]
sld = ".".join(labels[:-1])
else:
tld = ""
sld = domain
return sld, tld
# --- Commands -------------------------------------------------------------
def cmd_public_ip(_args):
print(get_public_ip())
def cmd_setup(_args):
print("=== Namecheap API Setup ===\n")
public_ip = get_public_ip()
info("Your public IP address is: " + _c("0;36", public_ip))
print()
print("Make sure this IP is whitelisted at:")
print(" https://ap.www.namecheap.com/settings/tools/apiaccess/")
print()
existing_user, existing_key = load_config()
if existing_user and existing_key:
info(f"Existing configuration found for user: {existing_user}")
print("\nTesting API connection...")
try:
api_request("domains.getList", {"PageSize": "1"})
success("API connection successful!")
except Exception as exc: # noqa: BLE001
err(f"API connection failed: {exc}")
print()
answer = input("Update stored credentials? [y/N]: ").strip().lower()
if answer not in ("y", "yes"):
info("Keeping existing credentials.")
return
print()
print("Enter your Namecheap credentials:\n")
api_user = input(" API Username: ").strip()
api_key = getpass.getpass(" API Key (hidden): ").strip()
print()
if not api_user or not api_key:
err("Both username and API key are required.")
sys.exit(1)
save_config(api_user, api_key)
success(f"Credentials saved to {CONFIG_FILE}")
print("\nTesting API connection...")
try:
# Use the just-entered credentials directly for the validation call.
os.environ["NAMECHEAP_API_USER"] = api_user
os.environ["NAMECHEAP_API_KEY"] = api_key
api_request("domains.getList", {"PageSize": "1"})
success("API connection successful!")
except Exception as exc: # noqa: BLE001
warn("API connection failed. Please verify:")
print(" 1. API access is enabled (ON) at the Namecheap settings page")
print(f" 2. IP address {public_ip} is whitelisted")
print(" 3. Your API key is correct")
print(f" (details: {exc})")
def cmd_domains_list(args):
params = {"ListType": args.type, "Page": str(args.page), "PageSize": str(args.page_size)}
if args.search:
params["SearchTerm"] = args.search
info("Fetching domain list...")
root = api_request("domains.getList", params)
print()
print(f"{'DOMAIN':<30} {'EXPIRES':<12} {'LOCKED':<12} {'AUTO-RENEW':<10}")
print(f"{'------':<30} {'-------':<12} {'------':<12} {'----------':<10}")
for d in root.iter("Domain"):
print("{:<30} {:<12} {:<12} {:<10}".format(
d.attrib.get("Name", ""),
d.attrib.get("Expires", ""),
d.attrib.get("IsLocked", ""),
d.attrib.get("AutoRenew", ""),
))
print()
def _print_hosts(root):
print()
print(f"{'HOST':<20} {'TYPE':<8} {'ADDRESS':<40} {'TTL':<8} {'MXPREF':<6}")
print(f"{'----':<20} {'----':<8} {'-------':<40} {'---':<8} {'------':<6}")
for h in root.iter("host"):
print("{:<20} {:<8} {:<40} {:<8} {:<6}".format(
h.attrib.get("Name", ""),
h.attrib.get("Type", ""),
h.attrib.get("Address", ""),
h.attrib.get("TTL", "1800"),
h.attrib.get("MXPref", "-"),
))
print()
def cmd_dns_get_hosts(args):
sld, tld = parse_domain(args.domain)
info(f"Fetching DNS records for {args.domain} (SLD={sld}, TLD={tld})...")
root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld})
_print_hosts(root)
def _existing_hosts(root):
"""Return existing host records as a list of dicts."""
records = []
for h in root.iter("host"):
records.append({
"name": h.attrib.get("Name", ""),
"type": h.attrib.get("Type", ""),
"address": h.attrib.get("Address", ""),
"ttl": h.attrib.get("TTL", "1800"),
"mxpref": h.attrib.get("MXPref", ""),
})
return records
def _hosts_to_params(sld, tld, records):
params = {"SLD": sld, "TLD": tld}
i = 1
for r in records:
if not (r["name"] and r["type"] and r["address"]):
continue
params[f"HostName{i}"] = r["name"]
params[f"RecordType{i}"] = r["type"]
params[f"Address{i}"] = r["address"]
params[f"TTL{i}"] = r.get("ttl") or "1800"
mxpref = r.get("mxpref")
if mxpref not in (None, ""):
# MX priority 0 is valid, so always send MXPref for MX records;
# for other record types only forward a non-zero value.
if r["type"].upper() == "MX" or mxpref != "0":
params[f"MXPref{i}"] = mxpref
i += 1
return params, i - 1
def cmd_dns_set_hosts(args):
if not os.path.isfile(args.hosts):
err(f"Hosts file not found: {args.hosts}")
sys.exit(1)
with open(args.hosts, "r", encoding="utf-8") as fh:
raw = json.load(fh)
records = []
for r in raw:
records.append({
"name": r.get("HostName", ""),
"type": r.get("RecordType", ""),
"address": r.get("Address", ""),
"ttl": str(r.get("TTL", "") or ""),
"mxpref": str(r.get("MXPref", "") or ""),
})
sld, tld = parse_domain(args.domain)
params, count = _hosts_to_params(sld, tld, records)
if count == 0:
err(f"No valid host records found in {args.hosts}")
sys.exit(1)
info(f"Setting {count} DNS records for {args.domain}...")
root = api_request("domains.dns.setHosts", params)
if _attr(root, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true":
success(f"DNS records updated successfully for {args.domain}!")
else:
err("Failed to update DNS records.")
sys.exit(1)
def cmd_dns_add_host(args):
sld, tld = parse_domain(args.domain)
info(f"Fetching existing DNS records for {args.domain}...")
root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld})
records = _existing_hosts(root)
records.append({
"name": args.name,
"type": args.type.upper(),
"address": args.address,
"ttl": args.ttl,
"mxpref": args.mxpref or "",
})
params, _ = _hosts_to_params(sld, tld, records)
info(f"Adding {args.type.upper()} record: {args.name} -> {args.address}")
result = api_request("domains.dns.setHosts", params)
if _attr(result, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true":
success(f"DNS record added: {args.name} {args.type} {args.address}")
else:
err("Failed to add DNS record.")
sys.exit(1)
def cmd_dns_remove_host(args):
sld, tld = parse_domain(args.domain)
info(f"Fetching existing DNS records for {args.domain}...")
root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld})
kept = []
removed = False
for r in _existing_hosts(root):
if (not removed and r["name"] == args.name
and r["type"].upper() == args.type.upper()
and (not args.address or r["address"] == args.address)):
removed = True
info(f"Removing record: {r['name']} {r['type']} {r['address']}")
continue
kept.append(r)
if not removed:
err("No matching record found to remove.")
sys.exit(1)
params, count = _hosts_to_params(sld, tld, kept)
if count == 0:
err("Cannot remove the last DNS record. Namecheap requires at least one record.")
sys.exit(1)
info(f"Updating DNS records for {args.domain}...")
result = api_request("domains.dns.setHosts", params)
if _attr(result, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true":
success("DNS record removed successfully!")
else:
err("Failed to remove DNS record.")
sys.exit(1)
def cmd_dns_get_list(args):
sld, tld = parse_domain(args.domain)
info(f"Fetching nameservers for {args.domain}...")
root = api_request("domains.dns.getList", {"SLD": sld, "TLD": tld})
using = _attr(root, "DomainDNSGetListResult", "IsUsingOurDNS", "unknown")
print()
info(f"Using Namecheap DNS: {using}")
print("\nNameservers:")
for ns in root.iter("Nameserver"):
if ns.text:
print(f" - {ns.text.strip()}")
print()
def cmd_dns_set_default(args):
sld, tld = parse_domain(args.domain)
info(f"Setting {args.domain} to use Namecheap default DNS...")
root = api_request("domains.dns.setDefault", {"SLD": sld, "TLD": tld})
if _attr(root, "DomainDNSSetDefaultResult", "Updated").lower() == "true":
success(f"Domain {args.domain} now uses Namecheap default DNS!")
else:
err("Failed to set default DNS.")
sys.exit(1)
def cmd_dns_set_custom(args):
sld, tld = parse_domain(args.domain)
info(f"Setting {args.domain} to use custom nameservers: {args.nameservers}")
root = api_request(
"domains.dns.setCustom",
{"SLD": sld, "TLD": tld, "Nameservers": args.nameservers},
)
if _attr(root, "DomainDNSSetCustomResult", "Updated").lower() == "true":
success(f"Domain {args.domain} now uses custom nameservers!")
else:
err("Failed to set custom nameservers.")
sys.exit(1)
def cmd_dns_get_email_forwarding(args):
info(f"Fetching email forwarding for {args.domain}...")
root = api_request("domains.dns.getEmailForwarding", {"DomainName": args.domain})
print()
print(f"{'MAILBOX':<20} {'FORWARDS TO':<40}")
print(f"{'-------':<20} {'-----------':<40}")
for fwd in root.iter("Forward"):
mailbox = (fwd.attrib.get("mailbox") or fwd.attrib.get("MailBox")
or fwd.attrib.get("Mailbox") or "")
forward_to = (fwd.attrib.get("ForwardTo") or fwd.attrib.get("forwardto")
or (fwd.text or "").strip())
print(f"{mailbox + '@' + args.domain:<20} {forward_to:<40}")
print()
def cmd_dns_set_email_forwarding(args):
params = {"DomainName": args.domain}
if args.mailbox and args.forward_to:
params["MailBox1"] = args.mailbox
params["ForwardTo1"] = args.forward_to
elif args.forwards:
if not os.path.isfile(args.forwards):
err(f"Forwards file not found: {args.forwards}")
sys.exit(1)
with open(args.forwards, "r", encoding="utf-8") as fh:
rules = json.load(fh)
i = 1
for rule in rules:
mailbox = rule.get("MailBox") or rule.get("mailbox") or ""
forward_to = rule.get("ForwardTo") or rule.get("forwardto") or ""
if mailbox and forward_to:
params[f"MailBox{i}"] = mailbox
params[f"ForwardTo{i}"] = forward_to
i += 1
else:
err("Provide either --mailbox/--forward-to or --forwards <file.json>")
sys.exit(1)
info(f"Setting email forwarding for {args.domain}...")
root = api_request("domains.dns.setEmailForwarding", params)
if _attr(root, "DomainDNSSetEmailForwardingResult", "IsSuccess").lower() == "true":
success(f"Email forwarding updated for {args.domain}!")
else:
err("Failed to set email forwarding.")
sys.exit(1)
def cmd_ns_create(args):
sld, tld = parse_domain(args.domain)
info(f"Creating nameserver {args.nameserver} -> {args.ip}...")
root = api_request(
"domains.ns.create",
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver, "IP": args.ip},
)
if _attr(root, "DomainNSCreateResult", "IsSuccess").lower() == "true":
success(f"Nameserver {args.nameserver} created!")
else:
err("Failed to create nameserver.")
sys.exit(1)
def cmd_ns_delete(args):
sld, tld = parse_domain(args.domain)
info(f"Deleting nameserver {args.nameserver}...")
root = api_request(
"domains.ns.delete",
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver},
)
if _attr(root, "DomainNSDeleteResult", "IsSuccess").lower() == "true":
success(f"Nameserver {args.nameserver} deleted!")
else:
err("Failed to delete nameserver.")
sys.exit(1)
def cmd_ns_get_info(args):
sld, tld = parse_domain(args.domain)
info(f"Fetching info for nameserver {args.nameserver}...")
root = api_request(
"domains.ns.getInfo",
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver},
)
ns_ip = _attr(root, "DomainNSInfoResult", "IP", "unknown")
print()
print(f"Nameserver: {args.nameserver}")
print(f"IP Address: {ns_ip}")
statuses = [s.text.strip() for s in root.iter("Status") if s.text and s.text.strip()]
if statuses:
print(f"Status: {', '.join(statuses)}")
print()
def cmd_ns_update(args):
sld, tld = parse_domain(args.domain)
info(f"Updating nameserver {args.nameserver}: {args.old_ip} -> {args.ip}...")
root = api_request(
"domains.ns.update",
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver,
"OldIP": args.old_ip, "IP": args.ip},
)
if _attr(root, "DomainNSUpdateResult", "IsSuccess").lower() == "true":
success(f"Nameserver {args.nameserver} updated to {args.ip}!")
else:
err("Failed to update nameserver.")
sys.exit(1)
# --- Argument parsing -----------------------------------------------------
def build_parser():
parser = argparse.ArgumentParser(
prog="namecheap.py",
description="Namecheap DNS Management CLI",
)
sub = parser.add_subparsers(dest="command", metavar="<command>")
sub.add_parser("setup", help="Configure API credentials and test connection").set_defaults(func=cmd_setup)
sub.add_parser("public-ip", help="Show your public IP address").set_defaults(func=cmd_public_ip)
p = sub.add_parser("domains.getList", help="List your Namecheap domains")
p.add_argument("--type", default="ALL")
p.add_argument("--search", default="")
p.add_argument("--page", type=int, default=1)
p.add_argument("--page-size", type=int, default=20)
p.set_defaults(func=cmd_domains_list)
p = sub.add_parser("domains.dns.getList", help="Get nameservers for a domain")
p.add_argument("--domain", required=True)
p.set_defaults(func=cmd_dns_get_list)
p = sub.add_parser("domains.dns.getHosts", help="Get DNS records for a domain")
p.add_argument("--domain", required=True)
p.set_defaults(func=cmd_dns_get_hosts)
p = sub.add_parser("domains.dns.setHosts", help="Set all DNS records (from JSON file)")
p.add_argument("--domain", required=True)
p.add_argument("--hosts", required=True)
p.set_defaults(func=cmd_dns_set_hosts)
p = sub.add_parser("domains.dns.setDefault", help="Use Namecheap default DNS")
p.add_argument("--domain", required=True)
p.set_defaults(func=cmd_dns_set_default)
p = sub.add_parser("domains.dns.setCustom", help="Use custom nameservers")
p.add_argument("--domain", required=True)
p.add_argument("--nameservers", required=True)
p.set_defaults(func=cmd_dns_set_custom)
p = sub.add_parser("domains.dns.getEmailForwarding", help="Get email forwarding rules")
p.add_argument("--domain", required=True)
p.set_defaults(func=cmd_dns_get_email_forwarding)
p = sub.add_parser("domains.dns.setEmailForwarding", help="Set email forwarding rules")
p.add_argument("--domain", required=True)
p.add_argument("--mailbox", default="")
p.add_argument("--forward-to", default="")
p.add_argument("--forwards", default="")
p.set_defaults(func=cmd_dns_set_email_forwarding)
p = sub.add_parser("domains.ns.create", help="Create a child nameserver (glue record)")
p.add_argument("--domain", required=True)
p.add_argument("--nameserver", required=True)
p.add_argument("--ip", required=True)
p.set_defaults(func=cmd_ns_create)
p = sub.add_parser("domains.ns.delete", help="Delete a child nameserver")
p.add_argument("--domain", required=True)
p.add_argument("--nameserver", required=True)
p.set_defaults(func=cmd_ns_delete)
p = sub.add_parser("domains.ns.getInfo", help="Get nameserver info")
p.add_argument("--domain", required=True)
p.add_argument("--nameserver", required=True)
p.set_defaults(func=cmd_ns_get_info)
p = sub.add_parser("domains.ns.update", help="Update nameserver IP")
p.add_argument("--domain", required=True)
p.add_argument("--nameserver", required=True)
p.add_argument("--old-ip", required=True)
p.add_argument("--ip", required=True)
p.set_defaults(func=cmd_ns_update)
p = sub.add_parser("dns.addHost", help="Add a single DNS record (preserves existing)")
p.add_argument("--domain", required=True)
p.add_argument("--type", required=True)
p.add_argument("--name", required=True)
p.add_argument("--address", required=True)
p.add_argument("--ttl", default="1800")
p.add_argument("--mxpref", default="")
p.set_defaults(func=cmd_dns_add_host)
p = sub.add_parser("dns.removeHost", help="Remove a single DNS record")
p.add_argument("--domain", required=True)
p.add_argument("--type", required=True)
p.add_argument("--name", required=True)
p.add_argument("--address", default="")
p.set_defaults(func=cmd_dns_remove_host)
return parser
def main(argv=None):
parser = build_parser()
args = parser.parse_args(argv)
if not getattr(args, "command", None):
parser.print_help()
return 1
try:
args.func(args)
except NamecheapError as exc:
err(f"API returned error: {exc}")
return 1
except urllib.error.URLError as exc:
err(f"Network error: {exc}")
return 1
except (OSError, ET.ParseError, json.JSONDecodeError) as exc:
err(str(exc))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
-919
View File
@@ -1,919 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Namecheap API CLI wrapper
# Usage: ./namecheap.sh <command> [options]
NAMECHEAP_API_URL="https://api.namecheap.com/xml.response"
CONFIG_FILE="$HOME/.namecheap-api"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
print_error() { echo -e "${RED}Error:${NC} $1" >&2; }
print_success() { echo -e "${GREEN}${NC} $1"; }
print_info() { echo -e "${CYAN}${NC} $1"; }
print_warn() { echo -e "${YELLOW}${NC} $1"; }
# Load configuration
load_config() {
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
}
# Check if credentials are configured
check_credentials() {
load_config
if [[ -z "${NAMECHEAP_API_USER:-}" || -z "${NAMECHEAP_API_KEY:-}" ]]; then
print_error "Namecheap API credentials not configured."
echo ""
echo "Run './namecheap.sh setup' to configure your credentials."
echo ""
echo "You need:"
echo " 1. Your Namecheap username"
echo " 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/"
echo " 3. Your public IP whitelisted in the API settings"
exit 1
fi
}
# Get public IP address
get_public_ip() {
curl -s https://api.ipify.org 2>/dev/null || curl -s https://ifconfig.me 2>/dev/null || echo "unknown"
}
# Make API request
api_request() {
local command="$1"
shift
local extra_params=("$@")
check_credentials
local client_ip
client_ip=$(get_public_ip)
local url="${NAMECHEAP_API_URL}?ApiUser=${NAMECHEAP_API_USER}&ApiKey=${NAMECHEAP_API_KEY}&UserName=${NAMECHEAP_API_USER}&Command=namecheap.${command}&ClientIp=${client_ip}"
for param in "${extra_params[@]}"; do
url="${url}&${param}"
done
local response
response=$(curl -s "$url")
# Check for errors in the response
if echo "$response" | grep -q 'Status="ERROR"'; then
local error_msg
error_msg=$(echo "$response" | grep -oP '(?<=<Err Code=")[^"]*"[^>]*>\K[^<]+' 2>/dev/null || echo "$response" | sed -n 's/.*<Err[^>]*>\(.*\)<\/Err>.*/\1/p')
print_error "API returned error: $error_msg"
return 1
fi
echo "$response"
}
# Parse domain into SLD and TLD
parse_domain() {
local domain="$1"
local tld sld
# Handle multi-part TLDs (e.g., co.uk, com.br)
if echo "$domain" | grep -qE '\.(co|com|net|org|gov)\.[a-z]{2}$'; then
tld=$(echo "$domain" | grep -oE '\.[^.]+\.[^.]+$' | sed 's/^\.//')
sld=$(echo "$domain" | sed "s/\.${tld}$//")
else
tld="${domain##*.}"
sld="${domain%.*}"
fi
echo "$sld" "$tld"
}
# Format XML DNS records as a table
format_dns_records() {
local xml="$1"
# Extract host records
echo ""
printf "%-20s %-8s %-40s %-8s %-6s\n" "HOST" "TYPE" "ADDRESS" "TTL" "MXPREF"
printf "%-20s %-8s %-40s %-8s %-6s\n" "----" "----" "-------" "---" "------"
echo "$xml" | grep -oP '<host[^/]*/>' | while read -r line; do
local name type address ttl mxpref
name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "")
address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "")
ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800")
mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "-")
printf "%-20s %-8s %-40s %-8s %-6s\n" "$name" "$type" "$address" "$ttl" "$mxpref"
done
echo ""
}
# Format domains list as a table
format_domains_list() {
local xml="$1"
echo ""
printf "%-30s %-12s %-12s %-10s\n" "DOMAIN" "EXPIRES" "LOCKED" "AUTO-RENEW"
printf "%-30s %-12s %-12s %-10s\n" "------" "-------" "------" "----------"
echo "$xml" | grep -oP '<Domain[^/]*/>' | while read -r line; do
local name expires locked autorenew
name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
expires=$(echo "$line" | grep -oP 'Expires="\K[^"]+' || echo "")
locked=$(echo "$line" | grep -oP 'IsLocked="\K[^"]+' || echo "")
autorenew=$(echo "$line" | grep -oP 'AutoRenew="\K[^"]+' || echo "")
printf "%-30s %-12s %-12s %-10s\n" "$name" "$expires" "$locked" "$autorenew"
done
echo ""
}
# Commands
cmd_setup() {
echo "=== Namecheap API Setup ==="
echo ""
# Show public IP
local public_ip
public_ip=$(get_public_ip)
print_info "Your public IP address is: ${CYAN}${public_ip}${NC}"
echo ""
echo "Make sure this IP is whitelisted at:"
echo " https://ap.www.namecheap.com/settings/tools/apiaccess/"
echo ""
# Check existing config
if [[ -f "$CONFIG_FILE" ]]; then
load_config
if [[ -n "${NAMECHEAP_API_USER:-}" ]]; then
print_info "Existing configuration found for user: ${NAMECHEAP_API_USER}"
echo ""
# Test the connection
echo "Testing API connection..."
if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then
print_success "API connection successful!"
else
print_error "API connection failed. Please check your credentials and IP whitelist."
fi
return 0
fi
fi
# Prompt for credentials
echo "Enter your Namecheap credentials:"
echo ""
read -rp " API Username: " api_user
read -rsp " API Key: " api_key
echo ""
echo ""
if [[ -z "$api_user" || -z "$api_key" ]]; then
print_error "Both username and API key are required."
exit 1
fi
# Save configuration
cat > "$CONFIG_FILE" << EOF
NAMECHEAP_API_USER="${api_user}"
NAMECHEAP_API_KEY="${api_key}"
EOF
chmod 600 "$CONFIG_FILE"
print_success "Credentials saved to ${CONFIG_FILE}"
echo ""
# Test connection
load_config
echo "Testing API connection..."
if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then
print_success "API connection successful!"
else
print_warn "API connection failed. Please verify:"
echo " 1. API access is enabled (ON) at the Namecheap settings page"
echo " 2. IP address ${public_ip} is whitelisted"
echo " 3. Your API key is correct"
fi
}
cmd_domains_list() {
local list_type="ALL"
local search_term=""
local page="1"
local page_size="20"
while [[ $# -gt 0 ]]; do
case "$1" in
--type) list_type="$2"; shift 2 ;;
--search) search_term="$2"; shift 2 ;;
--page) page="$2"; shift 2 ;;
--page-size) page_size="$2"; shift 2 ;;
*) shift ;;
esac
done
local params=("ListType=${list_type}" "Page=${page}" "PageSize=${page_size}")
if [[ -n "$search_term" ]]; then
params+=("SearchTerm=${search_term}")
fi
print_info "Fetching domain list..."
local response
response=$(api_request "domains.getList" "${params[@]}")
format_domains_list "$response"
}
cmd_dns_get_hosts() {
local domain=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" ]]; then
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getHosts --domain example.com"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Fetching DNS records for ${domain} (SLD=${sld}, TLD=${tld})..."
local response
response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}")
format_dns_records "$response"
}
cmd_dns_set_hosts() {
local domain=""
local hosts_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--hosts) hosts_file="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$hosts_file" ]]; then
print_error "Both --domain and --hosts are required."
echo "Usage: ./namecheap.sh domains.dns.setHosts --domain example.com --hosts hosts.json"
exit 1
fi
if [[ ! -f "$hosts_file" ]]; then
print_error "Hosts file not found: ${hosts_file}"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
# Build host parameters from JSON file
local params=("SLD=${sld}" "TLD=${tld}")
local i=1
while IFS= read -r line; do
local hostname recordtype address ttl mxpref
hostname=$(echo "$line" | grep -oP '"HostName"\s*:\s*"\K[^"]+' || echo "")
recordtype=$(echo "$line" | grep -oP '"RecordType"\s*:\s*"\K[^"]+' || echo "")
address=$(echo "$line" | grep -oP '"Address"\s*:\s*"\K[^"]+' || echo "")
ttl=$(echo "$line" | grep -oP '"TTL"\s*:\s*"\K[^"]+' || echo "1800")
mxpref=$(echo "$line" | grep -oP '"MXPref"\s*:\s*"\K[^"]+' || echo "")
if [[ -n "$hostname" && -n "$recordtype" && -n "$address" ]]; then
params+=("HostName${i}=${hostname}")
params+=("RecordType${i}=${recordtype}")
params+=("Address${i}=${address}")
params+=("TTL${i}=${ttl}")
if [[ -n "$mxpref" ]]; then
params+=("MXPref${i}=${mxpref}")
fi
((i++))
fi
done < <(python3 -c "
import json, sys
with open('${hosts_file}') as f:
records = json.load(f)
for r in records:
print(json.dumps(r))
" 2>/dev/null || jq -c '.[]' "$hosts_file")
if [[ $i -eq 1 ]]; then
print_error "No valid host records found in ${hosts_file}"
exit 1
fi
print_info "Setting $((i-1)) DNS records for ${domain}..."
local response
response=$(api_request "domains.dns.setHosts" "${params[@]}")
if echo "$response" | grep -q 'IsSuccess="true"'; then
print_success "DNS records updated successfully for ${domain}!"
else
print_error "Failed to update DNS records."
echo "$response"
fi
}
cmd_dns_add_host() {
local domain="" record_type="" name="" address="" ttl="1800" mxpref=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--type) record_type="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--address) address="$2"; shift 2 ;;
--ttl) ttl="$2"; shift 2 ;;
--mxpref) mxpref="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$record_type" || -z "$name" || -z "$address" ]]; then
print_error "Missing required parameters."
echo "Usage: ./namecheap.sh dns.addHost --domain example.com --type A --name \"@\" --address \"1.2.3.4\" [--ttl 1800] [--mxpref 10]"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
# Fetch existing records
print_info "Fetching existing DNS records for ${domain}..."
local response
response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}")
# Build params with existing records + new one
local params=("SLD=${sld}" "TLD=${tld}")
local i=1
# Parse existing records
while IFS= read -r line; do
if [[ -z "$line" ]]; then continue; fi
local h_name h_type h_address h_ttl h_mxpref
h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "")
h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "")
h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800")
h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "")
if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then
params+=("HostName${i}=${h_name}")
params+=("RecordType${i}=${h_type}")
params+=("Address${i}=${h_address}")
params+=("TTL${i}=${h_ttl}")
if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then
params+=("MXPref${i}=${h_mxpref}")
fi
((i++))
fi
done < <(echo "$response" | grep -oP '<host[^/]*/>')
# Add the new record
params+=("HostName${i}=${name}")
params+=("RecordType${i}=${record_type}")
params+=("Address${i}=${address}")
params+=("TTL${i}=${ttl}")
if [[ -n "$mxpref" ]]; then
params+=("MXPref${i}=${mxpref}")
fi
print_info "Adding ${record_type} record: ${name} -> ${address}"
local set_response
set_response=$(api_request "domains.dns.setHosts" "${params[@]}")
if echo "$set_response" | grep -q 'IsSuccess="true"'; then
print_success "DNS record added successfully!"
else
print_error "Failed to add DNS record."
echo "$set_response"
fi
}
cmd_dns_remove_host() {
local domain="" record_type="" name="" address=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--type) record_type="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--address) address="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$record_type" || -z "$name" ]]; then
print_error "Missing required parameters."
echo "Usage: ./namecheap.sh dns.removeHost --domain example.com --type A --name \"@\" [--address \"1.2.3.4\"]"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
# Fetch existing records
print_info "Fetching existing DNS records for ${domain}..."
local response
response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}")
# Build params excluding the record to remove
local params=("SLD=${sld}" "TLD=${tld}")
local i=1
local removed=false
while IFS= read -r line; do
if [[ -z "$line" ]]; then continue; fi
local h_name h_type h_address h_ttl h_mxpref
h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "")
h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "")
h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800")
h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "")
# Check if this is the record to remove
if [[ "$h_name" == "$name" && "$h_type" == "$record_type" && "$removed" == "false" ]]; then
if [[ -z "$address" || "$h_address" == "$address" ]]; then
removed=true
print_info "Removing record: ${h_name} ${h_type} ${h_address}"
continue
fi
fi
if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then
params+=("HostName${i}=${h_name}")
params+=("RecordType${i}=${h_type}")
params+=("Address${i}=${h_address}")
params+=("TTL${i}=${h_ttl}")
if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then
params+=("MXPref${i}=${h_mxpref}")
fi
((i++))
fi
done < <(echo "$response" | grep -oP '<host[^/]*/>')
if [[ "$removed" == "false" ]]; then
print_error "No matching record found to remove."
exit 1
fi
# If no records left, we still need at least one (Namecheap requirement)
if [[ $i -eq 1 ]]; then
print_error "Cannot remove the last DNS record. Namecheap requires at least one record."
exit 1
fi
print_info "Updating DNS records for ${domain}..."
local set_response
set_response=$(api_request "domains.dns.setHosts" "${params[@]}")
if echo "$set_response" | grep -q 'IsSuccess="true"'; then
print_success "DNS record removed successfully!"
else
print_error "Failed to remove DNS record."
echo "$set_response"
fi
}
cmd_public_ip() {
local ip
ip=$(get_public_ip)
echo "$ip"
}
cmd_dns_get_list() {
local domain=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" ]]; then
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getList --domain example.com"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Fetching nameservers for ${domain}..."
local response
response=$(api_request "domains.dns.getList" "SLD=${sld}" "TLD=${tld}")
local using_our_dns
using_our_dns=$(echo "$response" | grep -oP 'IsUsingOurDNS="\K[^"]+' || echo "unknown")
echo ""
print_info "Using Namecheap DNS: ${using_our_dns}"
echo ""
echo "Nameservers:"
echo "$response" | grep -oP '<Nameserver>\K[^<]+' | while read -r ns; do
echo " - ${ns}"
done
echo ""
}
cmd_dns_set_default() {
local domain=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" ]]; then
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.setDefault --domain example.com"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Setting ${domain} to use Namecheap default DNS..."
local response
response=$(api_request "domains.dns.setDefault" "SLD=${sld}" "TLD=${tld}")
if echo "$response" | grep -q 'Updated="true"'; then
print_success "Domain ${domain} now uses Namecheap default DNS!"
else
print_error "Failed to set default DNS."
echo "$response"
fi
}
cmd_dns_set_custom() {
local domain="" nameservers=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--nameservers) nameservers="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$nameservers" ]]; then
print_error "Both --domain and --nameservers are required."
echo "Usage: ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Setting ${domain} to use custom nameservers: ${nameservers}"
local response
response=$(api_request "domains.dns.setCustom" "SLD=${sld}" "TLD=${tld}" "Nameservers=${nameservers}")
if echo "$response" | grep -q 'Updated="true"'; then
print_success "Domain ${domain} now uses custom nameservers!"
else
print_error "Failed to set custom nameservers."
echo "$response"
fi
}
cmd_dns_get_email_forwarding() {
local domain=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" ]]; then
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getEmailForwarding --domain example.com"
exit 1
fi
print_info "Fetching email forwarding for ${domain}..."
local response
response=$(api_request "domains.dns.getEmailForwarding" "DomainName=${domain}")
echo ""
printf "%-20s %-40s\n" "MAILBOX" "FORWARDS TO"
printf "%-20s %-40s\n" "-------" "-----------"
echo "$response" | grep -oP '<Forward[^/]*/>' | while read -r line; do
local mailbox forward_to
mailbox=$(echo "$line" | grep -oP 'mailbox="\K[^"]+' || echo "")
forward_to=$(echo "$line" | grep -oP 'ForwardTo="\K[^"]+' || echo "")
printf "%-20s %-40s\n" "${mailbox}@${domain}" "$forward_to"
done
echo ""
}
cmd_dns_set_email_forwarding() {
local domain="" forwards_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--forwards) forwards_file="$2"; shift 2 ;;
--mailbox)
# Inline single forwarding rule
local inline_mailbox="$2"; shift 2 ;;
--forward-to)
local inline_forward_to="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" ]]; then
print_error "Domain is required."
echo "Usage: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com"
echo " or: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json"
exit 1
fi
local params=("DomainName=${domain}")
if [[ -n "${inline_mailbox:-}" && -n "${inline_forward_to:-}" ]]; then
# Single inline rule
params+=("MailBox1=${inline_mailbox}" "ForwardTo1=${inline_forward_to}")
elif [[ -n "$forwards_file" ]]; then
if [[ ! -f "$forwards_file" ]]; then
print_error "Forwards file not found: ${forwards_file}"
exit 1
fi
local i=1
while IFS= read -r line; do
local mailbox forward_to
mailbox=$(echo "$line" | grep -oP '"MailBox"\s*:\s*"\K[^"]+' || echo "")
forward_to=$(echo "$line" | grep -oP '"ForwardTo"\s*:\s*"\K[^"]+' || echo "")
if [[ -n "$mailbox" && -n "$forward_to" ]]; then
params+=("MailBox${i}=${mailbox}" "ForwardTo${i}=${forward_to}")
((i++))
fi
done < <(python3 -c "
import json, sys
with open('${forwards_file}') as f:
rules = json.load(f)
for r in rules:
print(json.dumps(r))
" 2>/dev/null || jq -c '.[]' "$forwards_file")
else
print_error "Provide either --mailbox/--forward-to or --forwards <file.json>"
exit 1
fi
print_info "Setting email forwarding for ${domain}..."
local response
response=$(api_request "domains.dns.setEmailForwarding" "${params[@]}")
if echo "$response" | grep -q 'IsSuccess="true"'; then
print_success "Email forwarding updated for ${domain}!"
else
print_error "Failed to set email forwarding."
echo "$response"
fi
}
cmd_ns_create() {
local domain="" nameserver="" ip=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--nameserver) nameserver="$2"; shift 2 ;;
--ip) ip="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$nameserver" || -z "$ip" ]]; then
print_error "Missing required parameters."
echo "Usage: ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Creating nameserver ${nameserver} -> ${ip}..."
local response
response=$(api_request "domains.ns.create" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "IP=${ip}")
if echo "$response" | grep -q 'IsSuccess="true"'; then
print_success "Nameserver ${nameserver} created!"
else
print_error "Failed to create nameserver."
echo "$response"
fi
}
cmd_ns_delete() {
local domain="" nameserver=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--nameserver) nameserver="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$nameserver" ]]; then
print_error "Missing required parameters."
echo "Usage: ./namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Deleting nameserver ${nameserver}..."
local response
response=$(api_request "domains.ns.delete" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}")
if echo "$response" | grep -q 'IsSuccess="true"'; then
print_success "Nameserver ${nameserver} deleted!"
else
print_error "Failed to delete nameserver."
echo "$response"
fi
}
cmd_ns_get_info() {
local domain="" nameserver=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--nameserver) nameserver="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$nameserver" ]]; then
print_error "Missing required parameters."
echo "Usage: ./namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Fetching info for nameserver ${nameserver}..."
local response
response=$(api_request "domains.ns.getInfo" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}")
local ns_ip
ns_ip=$(echo "$response" | grep -oP 'IP="\K[^"]+' || echo "unknown")
echo ""
echo "Nameserver: ${nameserver}"
echo "IP Address: ${ns_ip}"
local statuses
statuses=$(echo "$response" | grep -oP '<Status>\K[^<]+' | tr '\n' ', ' | sed 's/,$//')
if [[ -n "$statuses" ]]; then
echo "Status: ${statuses}"
fi
echo ""
}
cmd_ns_update() {
local domain="" nameserver="" old_ip="" new_ip=""
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--nameserver) nameserver="$2"; shift 2 ;;
--old-ip) old_ip="$2"; shift 2 ;;
--ip) new_ip="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$domain" || -z "$nameserver" || -z "$old_ip" || -z "$new_ip" ]]; then
print_error "Missing required parameters."
echo "Usage: ./namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8"
exit 1
fi
local sld tld
read -r sld tld <<< "$(parse_domain "$domain")"
print_info "Updating nameserver ${nameserver}: ${old_ip} -> ${new_ip}..."
local response
response=$(api_request "domains.ns.update" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "OldIP=${old_ip}" "IP=${new_ip}")
if echo "$response" | grep -q 'IsSuccess="true"'; then
print_success "Nameserver ${nameserver} updated to ${new_ip}!"
else
print_error "Failed to update nameserver."
echo "$response"
fi
}
# Help
cmd_help() {
echo "Namecheap DNS Management CLI"
echo ""
echo "Usage: ./namecheap.sh <command> [options]"
echo ""
echo "Commands:"
echo " setup Configure API credentials and test connection"
echo " public-ip Show your public IP address"
echo ""
echo " domains.getList List your Namecheap domains"
echo ""
echo " domains.dns.getList Get nameservers for a domain"
echo " domains.dns.getHosts Get DNS records for a domain"
echo " domains.dns.setHosts Set all DNS records (from JSON file)"
echo " domains.dns.setDefault Use Namecheap default DNS"
echo " domains.dns.setCustom Use custom nameservers"
echo " domains.dns.getEmailForwarding Get email forwarding rules"
echo " domains.dns.setEmailForwarding Set email forwarding rules"
echo ""
echo " domains.ns.create Create a child nameserver (glue record)"
echo " domains.ns.delete Delete a child nameserver"
echo " domains.ns.getInfo Get nameserver info"
echo " domains.ns.update Update nameserver IP"
echo ""
echo " dns.addHost Add a single DNS record (preserves existing)"
echo " dns.removeHost Remove a single DNS record"
echo ""
echo "Options:"
echo " --domain <domain> Domain name (e.g., example.com)"
echo " --type <type> Record type (A, AAAA, CNAME, MX, TXT, etc.)"
echo " --name <hostname> Host name (e.g., @, www, mail)"
echo " --address <value> Record value (IP or target)"
echo " --ttl <seconds> TTL in seconds (default: 1800)"
echo " --mxpref <priority> MX preference (for MX records)"
echo " --hosts <file.json> JSON file with host records"
echo " --nameservers <ns,...> Comma-separated nameservers"
echo " --nameserver <ns> Nameserver hostname"
echo " --ip <address> IP address for nameserver"
echo " --old-ip <address> Current IP (for ns.update)"
echo " --mailbox <name> Email mailbox name"
echo " --forward-to <email> Forward destination email"
echo " --forwards <file.json> JSON file with forwarding rules"
echo " --search <term> Search term for domain list"
echo " --page <n> Page number for domain list"
echo " --page-size <n> Page size for domain list (10-100)"
echo ""
echo "Examples:"
echo " ./namecheap.sh setup"
echo " ./namecheap.sh domains.getList"
echo " ./namecheap.sh domains.dns.getHosts --domain example.com"
echo " ./namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4"
echo " ./namecheap.sh dns.removeHost --domain example.com --type A --name www"
echo " ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com"
echo " ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com"
echo " ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4"
}
# Main dispatch
main() {
local command="${1:-help}"
shift || true
case "$command" in
setup) cmd_setup "$@" ;;
public-ip) cmd_public_ip "$@" ;;
domains.getList) cmd_domains_list "$@" ;;
domains.dns.getList) cmd_dns_get_list "$@" ;;
domains.dns.getHosts) cmd_dns_get_hosts "$@" ;;
domains.dns.setHosts) cmd_dns_set_hosts "$@" ;;
domains.dns.setDefault) cmd_dns_set_default "$@" ;;
domains.dns.setCustom) cmd_dns_set_custom "$@" ;;
domains.dns.getEmailForwarding) cmd_dns_get_email_forwarding "$@" ;;
domains.dns.setEmailForwarding) cmd_dns_set_email_forwarding "$@" ;;
domains.ns.create) cmd_ns_create "$@" ;;
domains.ns.delete) cmd_ns_delete "$@" ;;
domains.ns.getInfo) cmd_ns_get_info "$@" ;;
domains.ns.update) cmd_ns_update "$@" ;;
dns.addHost) cmd_dns_add_host "$@" ;;
dns.removeHost) cmd_dns_remove_host "$@" ;;
help|--help|-h) cmd_help ;;
*)
print_error "Unknown command: ${command}"
echo ""
cmd_help
exit 1
;;
esac
}
main "$@"