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
+1 -1
View File
@@ -245,7 +245,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [mvvm-toolkit](../skills/mvvm-toolkit/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit` | CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia. | `references/end-to-end-walkthrough.md`<br />`references/relaycommand-cookbook.md`<br />`references/source-generators.md`<br />`references/troubleshooting.md`<br />`references/validation.md` |
| [mvvm-toolkit-di](../skills/mvvm-toolkit-di/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit-di` | Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection. Covers the .NET Generic Host composition root, constructor injection, service lifetimes (Singleton / Transient / Scoped), IMessenger registration, resolving ViewModels in Views, keyed services, testing seams, and the legacy Ioc.Default escape hatch. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/dependency-injection.md` |
| [mvvm-toolkit-messenger](../skills/mvvm-toolkit-messenger/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit-messenger` | CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage<T> / AsyncRequestMessage<T> / CollectionRequestMessage<T>, ValueChangedMessage<T>, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/messenger-patterns.md` |
| [namecheap](../skills/namecheap/SKILL.md)<br />`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.sh`<br />`references/namecheap-api.md` |
| [namecheap](../skills/namecheap/SKILL.md)<br />`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.py`<br />`references/namecheap-api.md` |
| [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md)<br />`gh skills install github/awesome-copilot nano-banana-pro-openrouter` | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`<br />`scripts/generate_image.py` |
| [napkin](../skills/napkin/SKILL.md)<br />`gh skills install github/awesome-copilot napkin` | Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas. | `assets/napkin.html`<br />`assets/step1-activate.svg`<br />`assets/step2-whiteboard.svg`<br />`assets/step3-draw.svg`<br />`assets/step4-share.svg`<br />`assets/step5-response.svg` |
| [next-intl-add-language](../skills/next-intl-add-language/SKILL.md)<br />`gh skills install github/awesome-copilot next-intl-add-language` | Add new language to a Next.js + next-intl application | None |
+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 "$@"