use serde::{Deserialize, Serialize};
use crate::api::xml::{self, de_bool, de_date};
use crate::api::{Client, Error, Transport};
#[derive(Debug, Deserialize)]
pub struct GetListResponse {
#[serde(rename = "DomainGetListResult", default)]
result: DomainListResult,
#[serde(rename = "Paging")]
paging: Paging,
}
#[derive(Debug, Default, Deserialize)]
struct DomainListResult {
#[serde(rename = "Domain", default)]
domains: Vec<Domain>,
}
#[derive(Debug, Deserialize)]
struct Paging {
#[serde(rename = "TotalItems")]
total_items: usize,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Domain {
#[serde(rename(deserialize = "@ID", serialize = "id"))]
pub id: String,
#[serde(rename(deserialize = "@Name", serialize = "name"))]
pub name: String,
#[serde(rename(deserialize = "@User", serialize = "user"))]
pub user: String,
#[serde(
rename(deserialize = "@Created", serialize = "created"),
deserialize_with = "de_date"
)]
pub created: String,
#[serde(
rename(deserialize = "@Expires", serialize = "expires"),
deserialize_with = "de_date"
)]
pub expires: String,
#[serde(
rename(deserialize = "@IsExpired", serialize = "is_expired"),
deserialize_with = "de_bool"
)]
pub is_expired: bool,
#[serde(
rename(deserialize = "@IsLocked", serialize = "registry_hold"),
deserialize_with = "de_bool"
)]
pub registry_hold: bool,
#[serde(
rename(deserialize = "@AutoRenew", serialize = "auto_renew"),
deserialize_with = "de_bool"
)]
pub auto_renew: bool,
#[serde(rename(deserialize = "@WhoisGuard", serialize = "privacy"))]
pub privacy: String,
#[serde(
rename(deserialize = "@IsPremium", serialize = "is_premium"),
deserialize_with = "de_bool"
)]
pub is_premium: bool,
#[serde(
rename(deserialize = "@IsOurDNS", serialize = "is_our_dns"),
deserialize_with = "de_bool"
)]
pub is_our_dns: bool,
}
pub fn list<T: Transport>(client: &Client<T>) -> Result<Vec<Domain>, Error> {
let mut domains: Vec<Domain> = Vec::new();
let mut page = 1usize;
loop {
let body = client.call(
"domains.getList",
&[
("Page", page.to_string().as_str()),
("PageSize", "100"),
("SortBy", "NAME"),
],
)?;
let resp: GetListResponse = xml::parse(&body)?;
let before = domains.len();
domains.extend(resp.result.domains);
if domains.len() >= resp.paging.total_items {
return Ok(domains);
}
if domains.len() == before {
return Err(Error::Parse(format!(
"pagination stalled: server reports {} total items but page {page} added none",
resp.paging.total_items
)));
}
if page >= 100 {
return Err(Error::Parse(
"pagination overflow: 100 pages fetched without completing the listing".into(),
));
}
page += 1;
}
}
#[derive(Debug, Deserialize)]
pub struct CheckResponse {
#[serde(rename = "DomainCheckResult", default)]
results: Vec<CheckResult>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CheckResult {
#[serde(rename(deserialize = "@Domain", serialize = "domain"))]
pub domain: String,
#[serde(
rename(deserialize = "@Available", serialize = "available"),
deserialize_with = "de_bool"
)]
pub available: bool,
#[serde(
rename(deserialize = "@IsPremiumName", serialize = "is_premium"),
deserialize_with = "de_bool",
default
)]
pub is_premium: bool,
#[serde(
rename(
deserialize = "@PremiumRegistrationPrice",
serialize = "premium_registration_price"
),
default
)]
pub premium_registration_price: String,
#[serde(
rename(
deserialize = "@PremiumRenewalPrice",
serialize = "premium_renewal_price"
),
default
)]
pub premium_renewal_price: String,
#[serde(
rename(
deserialize = "@PremiumRestorePrice",
serialize = "premium_restore_price"
),
default
)]
pub premium_restore_price: String,
#[serde(
rename(
deserialize = "@PremiumTransferPrice",
serialize = "premium_transfer_price"
),
default
)]
pub premium_transfer_price: String,
#[serde(rename(deserialize = "@IcannFee", serialize = "icann_fee"), default)]
pub icann_fee: String,
#[serde(rename(deserialize = "@EapFee", serialize = "eap_fee"), default)]
pub eap_fee: String,
#[serde(rename(deserialize = "@ErrorNo", serialize = "error_no"), default)]
pub error_no: String,
#[serde(
rename(deserialize = "@Description", serialize = "description"),
default
)]
pub description: String,
}
pub fn check<T: Transport>(
client: &Client<T>,
domains: &[String],
) -> Result<Vec<CheckResult>, Error> {
if domains.len() > 50 {
return Err(Error::Usage(format!(
"domains check accepts at most 50 domains per call (got {})",
domains.len()
)));
}
let normalized: Vec<String> = domains
.iter()
.map(|d| crate::domain::normalize(d))
.collect::<Result<_, _>>()?;
let list = normalized.join(",");
let body = client.call("domains.check", &[("DomainList", &list)])?;
let resp: CheckResponse = xml::parse(&body)?;
Ok(resp.results)
}
#[derive(Debug, Deserialize)]
pub struct LockResponse {
#[serde(rename = "DomainGetRegistrarLockResult")]
result: LockStatus,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LockStatus {
#[serde(rename(deserialize = "@Domain", serialize = "domain"))]
pub domain: String,
#[serde(
rename(deserialize = "@RegistrarLockStatus", serialize = "locked"),
deserialize_with = "de_bool"
)]
pub locked: bool,
}
#[derive(Debug, Deserialize)]
pub struct SetLockResponse {
#[serde(rename = "DomainSetRegistrarLockResult")]
result: SetLockXml,
}
#[derive(Debug, Deserialize)]
struct SetLockXml {
#[serde(rename = "@Domain", default)]
domain: String,
#[serde(rename = "@IsSuccess", deserialize_with = "de_bool")]
is_success: bool,
}
#[derive(Debug, Serialize)]
pub struct SetLockResult {
pub domain: String,
pub locked: bool,
pub is_success: bool,
pub previously_locked: bool,
}
pub fn set_lock<T: Transport>(
client: &Client<T>,
domain: &str,
lock: bool,
) -> Result<SetLockResult, Error> {
client.require_mutations_permitted()?;
let domain = crate::domain::normalize(domain)?;
let before = lock_status(client, &domain)?;
client.journal_note(
"domains.lock.set",
serde_json::json!({
"domain": domain,
"previously_locked": before.locked,
"requested": if lock { "LOCK" } else { "UNLOCK" },
}),
);
let action = if lock { "LOCK" } else { "UNLOCK" };
let body = client.call_mut(
"domains.setRegistrarLock",
&[("DomainName", domain.as_str()), ("LockAction", action)],
)?;
let resp: SetLockResponse = xml::parse(&body)?;
Ok(SetLockResult {
domain: if resp.result.domain.is_empty() {
domain
} else {
resp.result.domain
},
locked: lock,
is_success: resp.result.is_success,
previously_locked: before.locked,
})
}
pub fn render_set_lock(result: &SetLockResult) {
crate::safe_println!(
"{}: registrar lock {} ({}; was {})",
result.domain,
if result.locked { "on" } else { "off" },
if result.is_success {
"success"
} else {
"NOT successful"
},
if result.previously_locked {
"on"
} else {
"off"
},
);
}
pub fn lock_status<T: Transport>(client: &Client<T>, domain: &str) -> Result<LockStatus, Error> {
let domain = crate::domain::normalize(domain)?;
let body = client.call("domains.getRegistrarLock", &[("DomainName", &domain)])?;
let resp: LockResponse = xml::parse(&body)?;
Ok(resp.result)
}
#[derive(Debug, Deserialize)]
pub struct GetInfoResponse {
#[serde(rename = "DomainGetInfoResult")]
result: InfoXml,
}
#[derive(Debug, Deserialize)]
struct InfoXml {
#[serde(rename = "@Status")]
status: String,
#[serde(rename = "@ID")]
id: String,
#[serde(rename = "@DomainName")]
domain_name: String,
#[serde(rename = "@OwnerName", default)]
owner_name: String,
#[serde(rename = "@IsOwner", deserialize_with = "de_bool", default)]
is_owner: bool,
#[serde(rename = "@IsPremium", deserialize_with = "de_bool", default)]
is_premium: bool,
#[serde(rename = "DomainDetails", default)]
details: InfoDetailsXml,
#[serde(rename = "Whoisguard")]
whoisguard: Option<WhoisguardXml>,
#[serde(rename = "DnsDetails")]
dns_details: Option<DnsDetailsXml>,
#[serde(rename = "Modificationrights")]
modification_rights: Option<ModRightsXml>,
}
#[derive(Debug, Default, Deserialize)]
struct InfoDetailsXml {
#[serde(rename = "CreatedDate", default, deserialize_with = "de_date")]
created: String,
#[serde(rename = "ExpiredDate", default, deserialize_with = "de_date")]
expires: String,
}
#[derive(Debug, Deserialize)]
struct WhoisguardXml {
#[serde(rename = "@Enabled", default)]
enabled: String,
#[serde(rename = "ID", default)]
id: String,
#[serde(rename = "ExpiredDate", default, deserialize_with = "de_date")]
expires: String,
}
#[derive(Debug, Deserialize)]
struct DnsDetailsXml {
#[serde(rename = "@ProviderType", default)]
provider_type: String,
#[serde(rename = "Nameserver", default)]
nameservers: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct ModRightsXml {
#[serde(rename = "@All", deserialize_with = "de_bool", default)]
all: bool,
}
#[derive(Debug, Serialize)]
pub struct DomainInfo {
pub domain: String,
pub id: String,
pub status: String,
pub owner: String,
pub is_owner: bool,
pub is_premium: bool,
pub created: String,
pub expires: String,
pub privacy: Option<PrivacyInfo>,
pub dns_provider: Option<String>,
pub nameservers: Vec<String>,
pub modification_rights_all: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct PrivacyInfo {
pub enabled: String,
pub id: String,
pub expires: String,
}
pub fn info<T: Transport>(client: &Client<T>, domain: &str) -> Result<DomainInfo, Error> {
let domain = crate::domain::normalize(domain)?;
let body = client.call("domains.getInfo", &[("DomainName", &domain)])?;
let resp: GetInfoResponse = xml::parse(&body)?;
let r = resp.result;
Ok(DomainInfo {
domain: r.domain_name,
id: r.id,
status: r.status,
owner: r.owner_name,
is_owner: r.is_owner,
is_premium: r.is_premium,
created: r.details.created,
expires: r.details.expires,
privacy: r.whoisguard.map(|w| PrivacyInfo {
enabled: w.enabled,
id: w.id,
expires: w.expires,
}),
dns_provider: r.dns_details.as_ref().map(|d| d.provider_type.clone()),
nameservers: r.dns_details.map(|d| d.nameservers).unwrap_or_default(),
modification_rights_all: r.modification_rights.map(|m| m.all),
})
}
#[derive(Debug, Deserialize)]
pub struct GetContactsResponse {
#[serde(rename = "DomainContactsResult")]
result: ContactsXml,
}
#[derive(Debug, Deserialize)]
struct ContactsXml {
#[serde(rename = "@Domain")]
domain: String,
#[serde(rename = "Registrant")]
registrant: Contact,
#[serde(rename = "Tech")]
tech: Contact,
#[serde(rename = "Admin")]
admin: Contact,
#[serde(rename = "AuxBilling")]
aux_billing: Contact,
}
#[derive(Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct Contact {
#[serde(
rename(deserialize = "@ReadOnly", serialize = "read_only"),
deserialize_with = "de_bool",
default
)]
pub read_only: bool,
#[serde(
rename(deserialize = "OrganizationName", serialize = "organization_name"),
default
)]
pub organization_name: String,
#[serde(rename(deserialize = "JobTitle", serialize = "job_title"), default)]
pub job_title: String,
#[serde(rename(deserialize = "FirstName", serialize = "first_name"), default)]
pub first_name: String,
#[serde(rename(deserialize = "LastName", serialize = "last_name"), default)]
pub last_name: String,
#[serde(rename(deserialize = "Address1", serialize = "address1"), default)]
pub address1: String,
#[serde(rename(deserialize = "Address2", serialize = "address2"), default)]
pub address2: String,
#[serde(rename(deserialize = "City", serialize = "city"), default)]
pub city: String,
#[serde(
rename(deserialize = "StateProvince", serialize = "state_province"),
default
)]
pub state_province: String,
#[serde(rename(deserialize = "PostalCode", serialize = "postal_code"), default)]
pub postal_code: String,
#[serde(rename(deserialize = "Country", serialize = "country"), default)]
pub country: String,
#[serde(rename(deserialize = "Phone", serialize = "phone"), default)]
pub phone: String,
#[serde(rename(deserialize = "PhoneExt", serialize = "phone_ext"), default)]
pub phone_ext: String,
#[serde(rename(deserialize = "Fax", serialize = "fax"), default)]
pub fax: String,
#[serde(
rename(deserialize = "EmailAddress", serialize = "email_address"),
default
)]
pub email_address: String,
}
#[derive(Debug, Serialize)]
pub struct Contacts {
pub domain: String,
pub registrant: Contact,
pub tech: Contact,
pub admin: Contact,
pub aux_billing: Contact,
}
pub fn contacts<T: Transport>(client: &Client<T>, domain: &str) -> Result<Contacts, Error> {
let domain = crate::domain::normalize(domain)?;
let body = client.call("domains.getContacts", &[("DomainName", &domain)])?;
let resp: GetContactsResponse = xml::parse(&body)?;
let r = resp.result;
Ok(Contacts {
domain: r.domain,
registrant: r.registrant,
tech: r.tech,
admin: r.admin,
aux_billing: r.aux_billing,
})
}
pub fn contacts_redacted_view(c: &Contacts) -> serde_json::Value {
let identical = c.registrant == c.tech && c.tech == c.admin && c.admin == c.aux_billing;
serde_json::json!({
"domain": c.domain,
"all_contact_sets_identical": identical,
"registrant": contact_summary(&c.registrant),
"tech": contact_summary(&c.tech),
"admin": contact_summary(&c.admin),
"aux_billing": contact_summary(&c.aux_billing),
})
}
fn contact_summary(c: &Contact) -> serde_json::Value {
serde_json::json!({
"country": c.country,
"state_province": c.state_province,
"read_only": c.read_only,
})
}
pub fn render_info(info: &DomainInfo) {
crate::safe_println!("domain: {}", info.domain);
crate::safe_println!("status: {}", info.status);
crate::safe_println!("owner: {} (is_owner: {})", info.owner, info.is_owner);
crate::safe_println!("created: {}", info.created);
crate::safe_println!("expires: {}", info.expires);
crate::safe_println!("premium: {}", if info.is_premium { "yes" } else { "no" });
match &info.privacy {
Some(p) => crate::safe_println!("privacy: {} (expires {})", p.enabled, p.expires),
None => crate::safe_println!("privacy: not reported"),
}
if let Some(provider) = &info.dns_provider {
crate::safe_println!("dns_provider: {provider}");
}
for ns in &info.nameservers {
crate::safe_println!("nameserver: {ns}");
}
if let Some(all) = info.modification_rights_all {
crate::safe_println!("modification_rights_all: {all}");
}
}
pub fn render_contacts(c: &Contacts, full: bool) {
crate::safe_println!("domain: {}", c.domain);
let sets: [(&str, &Contact); 4] = [
("registrant", &c.registrant),
("tech", &c.tech),
("admin", &c.admin),
("aux_billing", &c.aux_billing),
];
if full {
for (label, contact) in sets {
crate::safe_println!("[{label}]");
crate::safe_println!(" name: {} {}", contact.first_name, contact.last_name);
if !contact.organization_name.is_empty() {
crate::safe_println!(" organization: {}", contact.organization_name);
}
crate::safe_println!(" address: {} {}", contact.address1, contact.address2);
crate::safe_println!(
" locality: {} {} {} {}",
contact.city,
contact.state_province,
contact.postal_code,
contact.country
);
crate::safe_println!(
" phone: {} email: {}",
contact.phone,
contact.email_address
);
crate::safe_println!(" read_only: {}", contact.read_only);
}
} else {
let identical = c.registrant == c.tech && c.tech == c.admin && c.admin == c.aux_billing;
crate::safe_println!("all_contact_sets_identical: {identical}");
for (label, contact) in sets {
crate::safe_println!(
"{label}: country {} read_only {}",
contact.country,
contact.read_only
);
}
crate::safe_println!("(contact details redacted; pass --full to show them)");
}
}
pub fn render_check(results: &[CheckResult]) {
crate::safe_println!(
"{:<40} {:<10} {:<8} PRICE",
"DOMAIN",
"AVAILABLE",
"PREMIUM"
);
for r in results {
crate::safe_println!(
"{:<40} {:<10} {:<8} {}",
r.domain,
if r.available { "yes" } else { "no" },
if r.is_premium { "yes" } else { "no" },
if r.is_premium {
r.premium_registration_price.as_str()
} else {
"-"
},
);
}
}
pub fn render_lock(status: &LockStatus) {
crate::safe_println!(
"{}: registrar lock {}",
status.domain,
if status.locked { "on" } else { "off" }
);
}
pub fn render_table(domains: &[Domain]) {
crate::safe_println!(
"{:<40} {:<12} {:<6} {:<6} {:<12} {:<7}",
"NAME",
"EXPIRES",
"HOLD",
"RENEW",
"PRIVACY",
"OURDNS"
);
for d in domains {
crate::safe_println!(
"{:<40} {:<12} {:<6} {:<6} {:<12} {:<7}",
d.name,
d.expires,
if d.registry_hold { "yes" } else { "no" },
if d.auto_renew { "yes" } else { "no" },
d.privacy,
if d.is_our_dns { "yes" } else { "no" },
);
}
}
#[derive(Debug, Deserialize)]
pub struct CreateResponse {
#[serde(rename = "DomainCreateResult")]
result: CreateXml,
}
#[derive(Debug, Deserialize)]
struct CreateXml {
#[serde(rename = "@Domain", default)]
domain: String,
#[serde(rename = "@Registered", deserialize_with = "de_bool")]
registered: bool,
#[serde(rename = "@ChargedAmount")]
charged_amount: String,
#[serde(rename = "@DomainID")]
domain_id: String,
#[serde(rename = "@OrderID")]
order_id: String,
#[serde(rename = "@TransactionID")]
transaction_id: String,
}
#[derive(Debug, Serialize)]
pub struct RegisterResult {
pub domain: String,
pub registered: bool,
pub years: u8,
pub listed_price: String,
pub charged_amount: String,
pub charged_exceeded_max_price: bool,
pub domain_id: String,
pub order_id: String,
pub transaction_id: String,
}
pub fn register<T: Transport>(
client: &Client<T>,
domain: &str,
years: u8,
max_price: f64,
contacts_from: &str,
) -> Result<RegisterResult, Error> {
client.require_mutations_permitted()?;
let domain = crate::domain::normalize(domain)?;
let (_, tld) = crate::domain::split_sld_tld(&domain)?;
let availability = check(client, std::slice::from_ref(&domain))?;
let status = availability
.first()
.ok_or_else(|| Error::Parse("empty availability response".into()))?;
if !status.available {
return Err(Error::Usage(format!(
"{domain} is not available for registration"
)));
}
if status.is_premium {
return Err(Error::Usage(format!(
"{domain} is a premium domain; ncheap does not register premium domains"
)));
}
if status
.eap_fee
.parse::<f64>()
.map(|f| f > 0.0)
.unwrap_or(false)
{
return Err(Error::Usage(format!(
"{domain} carries an Early Access Phase fee ({}); ncheap does not register EAP domains",
status.eap_fee
)));
}
let listed = crate::commands::account::live_price(client, &tld, "REGISTER", years)?;
if listed > max_price {
return Err(Error::Usage(format!(
"listed price {listed:.2} for {years} year(s) exceeds --max-price {max_price:.2}; not registering"
)));
}
let source = contacts(client, contacts_from)?;
let mut params: Vec<(String, String)> = vec![
("DomainName".into(), domain.clone()),
("Years".into(), years.to_string()),
("AddFreeWhoisguard".into(), "yes".into()),
("WGEnabled".into(), "no".into()),
];
for (role, contact) in [
("Registrant", &source.registrant),
("Tech", &source.tech),
("Admin", &source.admin),
("AuxBilling", &source.aux_billing),
] {
contact_params(role, contact, &mut params);
}
let param_refs: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
client.reserve_spend(listed, "domains.create", &domain)?;
let body = client.call_mut("domains.create", ¶m_refs)?;
let resp: CreateResponse = xml::parse(&body)?;
let charged_exceeded_max_price = charge_exceeds(&resp.result.charged_amount, max_price);
Ok(RegisterResult {
domain: resp.result.domain,
registered: resp.result.registered,
years,
listed_price: format!("{listed:.2}"),
charged_amount: resp.result.charged_amount,
charged_exceeded_max_price,
domain_id: resp.result.domain_id,
order_id: resp.result.order_id,
transaction_id: resp.result.transaction_id,
})
}
fn contact_params(role: &str, c: &Contact, out: &mut Vec<(String, String)>) {
let required = [
("FirstName", &c.first_name),
("LastName", &c.last_name),
("Address1", &c.address1),
("City", &c.city),
("StateProvince", &c.state_province),
("PostalCode", &c.postal_code),
("Country", &c.country),
("Phone", &c.phone),
("EmailAddress", &c.email_address),
];
for (key, value) in required {
out.push((format!("{role}{key}"), value.clone()));
}
let optional = [
("OrganizationName", &c.organization_name),
("JobTitle", &c.job_title),
("Address2", &c.address2),
("PhoneExt", &c.phone_ext),
("Fax", &c.fax),
];
for (key, value) in optional {
if !value.is_empty() {
out.push((format!("{role}{key}"), value.clone()));
}
}
}
#[derive(Debug, Deserialize)]
pub struct RenewResponse {
#[serde(rename = "DomainRenewResult")]
result: RenewXml,
}
#[derive(Debug, Deserialize)]
struct RenewXml {
#[serde(rename = "@DomainName", default)]
domain_name: String,
#[serde(rename = "@Renew", deserialize_with = "de_bool")]
renew: bool,
#[serde(rename = "@ChargedAmount")]
charged_amount: String,
#[serde(rename = "@DomainID")]
domain_id: String,
#[serde(rename = "@OrderID")]
order_id: String,
#[serde(rename = "@TransactionID")]
transaction_id: String,
}
#[derive(Debug, Serialize)]
pub struct RenewResult {
pub domain: String,
pub renewed: bool,
pub years: u8,
pub listed_price: String,
pub charged_amount: String,
pub charged_exceeded_max_price: bool,
pub domain_id: String,
pub order_id: String,
pub transaction_id: String,
}
fn charge_exceeds(charged: &str, max_price: f64) -> bool {
charged
.parse::<f64>()
.map(|c| c > max_price)
.unwrap_or(false)
}
pub fn renew<T: Transport>(
client: &Client<T>,
domain: &str,
years: u8,
max_price: f64,
) -> Result<RenewResult, Error> {
client.require_mutations_permitted()?;
let domain = crate::domain::normalize(domain)?;
let (_, tld) = crate::domain::split_sld_tld(&domain)?;
let listed = crate::commands::account::live_price(client, &tld, "RENEW", years)?;
if listed > max_price {
return Err(Error::Usage(format!(
"listed price {listed:.2} for {years} year(s) exceeds --max-price {max_price:.2}; not renewing"
)));
}
client.reserve_spend(listed, "domains.renew", &domain)?;
let years_str = years.to_string();
let body = client.call_mut(
"domains.renew",
&[
("DomainName", domain.as_str()),
("Years", years_str.as_str()),
],
)?;
let resp: RenewResponse = xml::parse(&body)?;
let charged_exceeded_max_price = charge_exceeds(&resp.result.charged_amount, max_price);
Ok(RenewResult {
domain: resp.result.domain_name,
renewed: resp.result.renew,
years,
listed_price: format!("{listed:.2}"),
charged_amount: resp.result.charged_amount,
charged_exceeded_max_price,
domain_id: resp.result.domain_id,
order_id: resp.result.order_id,
transaction_id: resp.result.transaction_id,
})
}
pub fn render_register(r: &RegisterResult) {
crate::safe_println!(
"{}: {} for {} year(s) — listed {}, charged {} (order {}, transaction {})",
r.domain,
if r.registered {
"registered"
} else {
"NOT registered"
},
r.years,
r.listed_price,
r.charged_amount,
r.order_id,
r.transaction_id,
);
}
pub fn render_renew(r: &RenewResult) {
crate::safe_println!(
"{}: {} for {} year(s) — listed {}, charged {} (order {}, transaction {})",
r.domain,
if r.renewed { "renewed" } else { "NOT renewed" },
r.years,
r.listed_price,
r.charged_amount,
r.order_id,
r.transaction_id,
);
}
#[derive(Debug, Deserialize)]
pub struct SetContactsResponse {
#[serde(rename = "DomainSetContactResult")]
result: SetContactsXml,
}
#[derive(Debug, Deserialize)]
struct SetContactsXml {
#[serde(rename = "@Domain", default)]
domain: String,
#[serde(rename = "@IsSuccess", deserialize_with = "de_bool")]
is_success: bool,
}
#[derive(Debug, Serialize)]
pub struct SetContactsResult {
pub domain: String,
pub is_success: bool,
pub copied_from: String,
}
pub fn set_contacts_from<T: Transport>(
client: &Client<T>,
domain: &str,
source: &str,
) -> Result<SetContactsResult, Error> {
client.require_mutations_permitted()?;
let domain = crate::domain::normalize(domain)?;
let before = contacts(client, &domain)?;
client.journal_note(
"domains.contacts.set",
serde_json::json!({
"domain": domain,
"copied_from": source,
"previous_contacts": serde_json::to_value(&before).unwrap_or_default(),
}),
);
let new = contacts(client, source)?;
let mut params: Vec<(String, String)> = vec![("DomainName".into(), domain.clone())];
for (role, contact) in [
("Registrant", &new.registrant),
("Tech", &new.tech),
("Admin", &new.admin),
("AuxBilling", &new.aux_billing),
] {
contact_params(role, contact, &mut params);
}
let param_refs: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let body = client.call_mut("domains.setContacts", ¶m_refs)?;
let resp: SetContactsResponse = xml::parse(&body)?;
Ok(SetContactsResult {
domain: if resp.result.domain.is_empty() {
domain
} else {
resp.result.domain
},
is_success: resp.result.is_success,
copied_from: source.to_owned(),
})
}
pub fn render_set_contacts(result: &SetContactsResult) {
crate::safe_println!(
"{}: contacts replaced from {} ({})",
result.domain,
result.copied_from,
if result.is_success {
"success"
} else {
"NOT successful"
},
);
}