1use chrono::TimeDelta;
2use colored::Colorize;
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use super::OutputFormatter;
7use crate::colors::CatppuccinExt;
8use crate::dns::{DnsRecord, FollowIteration, FollowResult, PropagationResult};
9use crate::lookup::LookupResult;
10use crate::rdap::RdapResponse;
11use crate::status::StatusResponse;
12use crate::whois::WhoisResponse;
13
14static ANSI_ESCAPE_RE: Lazy<Regex> = Lazy::new(|| {
17 Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[A-Z@-_]")
18 .expect("Invalid ANSI escape regex")
19});
20
21fn sanitize_display(s: &str) -> String {
22 ANSI_ESCAPE_RE.replace_all(s, "").to_string()
23}
24
25fn format_duration(duration: TimeDelta) -> String {
26 let total_secs = duration.num_seconds();
27 if total_secs < 60 {
28 format!("{}s", total_secs)
29 } else if total_secs < 3600 {
30 let mins = total_secs / 60;
31 let secs = total_secs % 60;
32 format!("{}m {}s", mins, secs)
33 } else {
34 let hours = total_secs / 3600;
35 let mins = (total_secs % 3600) / 60;
36 format!("{}h {}m", hours, mins)
37 }
38}
39
40pub struct HumanFormatter {
41 use_colors: bool,
42}
43
44impl Default for HumanFormatter {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl HumanFormatter {
51 pub fn new() -> Self {
52 Self { use_colors: true }
53 }
54
55 pub fn without_colors(mut self) -> Self {
56 self.use_colors = false;
57 self
58 }
59
60 fn label(&self, text: &str) -> String {
61 if self.use_colors {
62 text.sky().bold().to_string()
63 } else {
64 text.to_string()
65 }
66 }
67
68 fn value(&self, text: &str) -> String {
69 if self.use_colors {
70 text.ctp_white().to_string()
71 } else {
72 text.to_string()
73 }
74 }
75
76 fn success(&self, text: &str) -> String {
77 if self.use_colors {
78 text.ctp_green().bold().to_string()
79 } else {
80 text.to_string()
81 }
82 }
83
84 fn warning(&self, text: &str) -> String {
85 if self.use_colors {
86 text.ctp_yellow().bold().to_string()
87 } else {
88 text.to_string()
89 }
90 }
91
92 fn error(&self, text: &str) -> String {
93 if self.use_colors {
94 text.ctp_red().bold().to_string()
95 } else {
96 text.to_string()
97 }
98 }
99
100 fn header(&self, text: &str) -> String {
101 if self.use_colors {
102 format!(
103 "\n{}\n{}",
104 text.lavender().bold(),
105 "─".repeat(text.len()).subtext0()
106 )
107 } else {
108 format!("\n{}\n{}", text, "-".repeat(text.len()))
109 }
110 }
111}
112
113impl OutputFormatter for HumanFormatter {
114 fn format_whois(&self, response: &WhoisResponse) -> String {
115 let mut output = Vec::new();
116
117 output.push(self.header(&format!("WHOIS: {}", sanitize_display(&response.domain))));
118
119 if response.is_available() {
120 output.push(format!(" {} Domain is available", self.success("✓")));
121 return output.join("\n");
122 }
123
124 if let Some(ref registrar) = response.registrar {
125 output.push(format!(
126 " {}: {}",
127 self.label("Registrar"),
128 self.value(&sanitize_display(registrar))
129 ));
130 }
131
132 if let Some(ref registrant) = response.registrant {
133 output.push(format!(
134 " {}: {}",
135 self.label("Registrant"),
136 self.value(&sanitize_display(registrant))
137 ));
138 }
139
140 if let Some(ref organization) = response.organization {
141 output.push(format!(
142 " {}: {}",
143 self.label("Organization"),
144 self.value(&sanitize_display(organization))
145 ));
146 }
147
148 let has_registrant_details = response.registrant_email.is_some()
150 || response.registrant_phone.is_some()
151 || response.registrant_address.is_some()
152 || response.registrant_country.is_some();
153
154 if has_registrant_details {
155 output.push(format!("\n {}:", self.label("Registrant Contact")));
156 if let Some(ref email) = response.registrant_email {
157 output.push(format!(
158 " {}: {}",
159 self.label("Email"),
160 self.value(&sanitize_display(email))
161 ));
162 }
163 if let Some(ref phone) = response.registrant_phone {
164 output.push(format!(
165 " {}: {}",
166 self.label("Phone"),
167 self.value(&sanitize_display(phone))
168 ));
169 }
170 if let Some(ref address) = response.registrant_address {
171 output.push(format!(
172 " {}: {}",
173 self.label("Address"),
174 self.value(&sanitize_display(address))
175 ));
176 }
177 if let Some(ref country) = response.registrant_country {
178 output.push(format!(
179 " {}: {}",
180 self.label("Country"),
181 self.value(&sanitize_display(country))
182 ));
183 }
184 }
185
186 let has_admin_contact = response.admin_name.is_some()
188 || response.admin_organization.is_some()
189 || response.admin_email.is_some()
190 || response.admin_phone.is_some();
191
192 if has_admin_contact {
193 output.push(format!("\n {}:", self.label("Admin Contact")));
194 if let Some(ref name) = response.admin_name {
195 output.push(format!(
196 " {}: {}",
197 self.label("Name"),
198 self.value(&sanitize_display(name))
199 ));
200 }
201 if let Some(ref org) = response.admin_organization {
202 output.push(format!(
203 " {}: {}",
204 self.label("Organization"),
205 self.value(&sanitize_display(org))
206 ));
207 }
208 if let Some(ref email) = response.admin_email {
209 output.push(format!(
210 " {}: {}",
211 self.label("Email"),
212 self.value(&sanitize_display(email))
213 ));
214 }
215 if let Some(ref phone) = response.admin_phone {
216 output.push(format!(
217 " {}: {}",
218 self.label("Phone"),
219 self.value(&sanitize_display(phone))
220 ));
221 }
222 }
223
224 let has_tech_contact = response.tech_name.is_some()
226 || response.tech_organization.is_some()
227 || response.tech_email.is_some()
228 || response.tech_phone.is_some();
229
230 if has_tech_contact {
231 output.push(format!("\n {}:", self.label("Tech Contact")));
232 if let Some(ref name) = response.tech_name {
233 output.push(format!(
234 " {}: {}",
235 self.label("Name"),
236 self.value(&sanitize_display(name))
237 ));
238 }
239 if let Some(ref org) = response.tech_organization {
240 output.push(format!(
241 " {}: {}",
242 self.label("Organization"),
243 self.value(&sanitize_display(org))
244 ));
245 }
246 if let Some(ref email) = response.tech_email {
247 output.push(format!(
248 " {}: {}",
249 self.label("Email"),
250 self.value(&sanitize_display(email))
251 ));
252 }
253 if let Some(ref phone) = response.tech_phone {
254 output.push(format!(
255 " {}: {}",
256 self.label("Phone"),
257 self.value(&sanitize_display(phone))
258 ));
259 }
260 }
261
262 if let Some(created) = response.creation_date {
263 output.push(format!(
264 " {}: {}",
265 self.label("Created"),
266 self.value(&created.format("%Y-%m-%d").to_string())
267 ));
268 }
269
270 if let Some(expires) = response.expiration_date {
271 let days_until = (expires - chrono::Utc::now()).num_days();
272 let expiry_str = expires.format("%Y-%m-%d").to_string();
273 let status = if days_until < 30 {
274 self.error(&format!("{} (expires in {} days!)", expiry_str, days_until))
275 } else if days_until < 90 {
276 self.warning(&format!("{} ({} days)", expiry_str, days_until))
277 } else {
278 self.value(&format!("{} ({} days)", expiry_str, days_until))
279 };
280 output.push(format!(" {}: {}", self.label("Expires"), status));
281 }
282
283 if let Some(updated) = response.updated_date {
284 output.push(format!(
285 " {}: {}",
286 self.label("Updated"),
287 self.value(&updated.format("%Y-%m-%d").to_string())
288 ));
289 }
290
291 if !response.nameservers.is_empty() {
292 output.push(format!(" {}:", self.label("Nameservers")));
293 for ns in &response.nameservers {
294 output.push(format!(" - {}", self.value(&sanitize_display(ns))));
295 }
296 }
297
298 if !response.status.is_empty() {
299 output.push(format!(" {}:", self.label("Status")));
300 for status in &response.status {
301 output.push(format!(" - {}", self.value(&sanitize_display(status))));
302 }
303 }
304
305 if let Some(ref dnssec) = response.dnssec {
306 output.push(format!(
307 " {}: {}",
308 self.label("DNSSEC"),
309 self.value(&sanitize_display(dnssec))
310 ));
311 }
312
313 output.push(format!(
314 " {}: {}",
315 self.label("WHOIS Server"),
316 self.value(&sanitize_display(&response.whois_server))
317 ));
318
319 output.join("\n")
320 }
321
322 fn format_rdap(&self, response: &RdapResponse) -> String {
323 let mut output = Vec::new();
324
325 let name = response
326 .domain_name()
327 .or(response.name.as_deref())
328 .unwrap_or("Unknown");
329 output.push(self.header(&format!("RDAP: {}", sanitize_display(name))));
330
331 if let Some(handle) = &response.handle {
332 output.push(format!(
333 " {}: {}",
334 self.label("Handle"),
335 self.value(&sanitize_display(handle))
336 ));
337 }
338
339 if let Some(registrar) = response.get_registrar() {
340 output.push(format!(
341 " {}: {}",
342 self.label("Registrar"),
343 self.value(&sanitize_display(®istrar))
344 ));
345 }
346
347 if let Some(registrant) = response.get_registrant() {
348 output.push(format!(
349 " {}: {}",
350 self.label("Registrant"),
351 self.value(&sanitize_display(®istrant))
352 ));
353 }
354
355 if let Some(organization) = response.get_registrant_organization() {
356 output.push(format!(
357 " {}: {}",
358 self.label("Organization"),
359 self.value(&sanitize_display(&organization))
360 ));
361 }
362
363 if let Some(contact) = response.get_registrant_contact() {
365 if contact.has_info() {
366 output.push(format!("\n {}:", self.label("Registrant Contact")));
367 if let Some(ref email) = contact.email {
368 output.push(format!(
369 " {}: {}",
370 self.label("Email"),
371 self.value(&sanitize_display(email))
372 ));
373 }
374 if let Some(ref phone) = contact.phone {
375 output.push(format!(
376 " {}: {}",
377 self.label("Phone"),
378 self.value(&sanitize_display(phone))
379 ));
380 }
381 if let Some(ref address) = contact.address {
382 output.push(format!(
383 " {}: {}",
384 self.label("Address"),
385 self.value(&sanitize_display(address))
386 ));
387 }
388 if let Some(ref country) = contact.country {
389 output.push(format!(
390 " {}: {}",
391 self.label("Country"),
392 self.value(&sanitize_display(country))
393 ));
394 }
395 }
396 }
397
398 if let Some(contact) = response.get_admin_contact() {
400 if contact.has_info() {
401 output.push(format!("\n {}:", self.label("Admin Contact")));
402 if let Some(ref name) = contact.name {
403 output.push(format!(
404 " {}: {}",
405 self.label("Name"),
406 self.value(&sanitize_display(name))
407 ));
408 }
409 if let Some(ref org) = contact.organization {
410 output.push(format!(
411 " {}: {}",
412 self.label("Organization"),
413 self.value(&sanitize_display(org))
414 ));
415 }
416 if let Some(ref email) = contact.email {
417 output.push(format!(
418 " {}: {}",
419 self.label("Email"),
420 self.value(&sanitize_display(email))
421 ));
422 }
423 if let Some(ref phone) = contact.phone {
424 output.push(format!(
425 " {}: {}",
426 self.label("Phone"),
427 self.value(&sanitize_display(phone))
428 ));
429 }
430 if let Some(ref address) = contact.address {
431 output.push(format!(
432 " {}: {}",
433 self.label("Address"),
434 self.value(&sanitize_display(address))
435 ));
436 }
437 if let Some(ref country) = contact.country {
438 output.push(format!(
439 " {}: {}",
440 self.label("Country"),
441 self.value(&sanitize_display(country))
442 ));
443 }
444 }
445 }
446
447 if let Some(contact) = response.get_tech_contact() {
449 if contact.has_info() {
450 output.push(format!("\n {}:", self.label("Tech Contact")));
451 if let Some(ref name) = contact.name {
452 output.push(format!(
453 " {}: {}",
454 self.label("Name"),
455 self.value(&sanitize_display(name))
456 ));
457 }
458 if let Some(ref org) = contact.organization {
459 output.push(format!(
460 " {}: {}",
461 self.label("Organization"),
462 self.value(&sanitize_display(org))
463 ));
464 }
465 if let Some(ref email) = contact.email {
466 output.push(format!(
467 " {}: {}",
468 self.label("Email"),
469 self.value(&sanitize_display(email))
470 ));
471 }
472 if let Some(ref phone) = contact.phone {
473 output.push(format!(
474 " {}: {}",
475 self.label("Phone"),
476 self.value(&sanitize_display(phone))
477 ));
478 }
479 if let Some(ref address) = contact.address {
480 output.push(format!(
481 " {}: {}",
482 self.label("Address"),
483 self.value(&sanitize_display(address))
484 ));
485 }
486 if let Some(ref country) = contact.country {
487 output.push(format!(
488 " {}: {}",
489 self.label("Country"),
490 self.value(&sanitize_display(country))
491 ));
492 }
493 }
494 }
495
496 if let Some(contact) = response.get_billing_contact() {
498 if contact.has_info() {
499 output.push(format!("\n {}:", self.label("Billing Contact")));
500 if let Some(ref name) = contact.name {
501 output.push(format!(
502 " {}: {}",
503 self.label("Name"),
504 self.value(&sanitize_display(name))
505 ));
506 }
507 if let Some(ref org) = contact.organization {
508 output.push(format!(
509 " {}: {}",
510 self.label("Organization"),
511 self.value(&sanitize_display(org))
512 ));
513 }
514 if let Some(ref email) = contact.email {
515 output.push(format!(
516 " {}: {}",
517 self.label("Email"),
518 self.value(&sanitize_display(email))
519 ));
520 }
521 if let Some(ref phone) = contact.phone {
522 output.push(format!(
523 " {}: {}",
524 self.label("Phone"),
525 self.value(&sanitize_display(phone))
526 ));
527 }
528 if let Some(ref address) = contact.address {
529 output.push(format!(
530 " {}: {}",
531 self.label("Address"),
532 self.value(&sanitize_display(address))
533 ));
534 }
535 if let Some(ref country) = contact.country {
536 output.push(format!(
537 " {}: {}",
538 self.label("Country"),
539 self.value(&sanitize_display(country))
540 ));
541 }
542 }
543 }
544
545 if let Some(created) = response.creation_date() {
546 output.push(format!(
547 " {}: {}",
548 self.label("Created"),
549 self.value(&created.format("%Y-%m-%d").to_string())
550 ));
551 }
552
553 if let Some(expires) = response.expiration_date() {
554 let days_until = (expires - chrono::Utc::now()).num_days();
555 let expiry_str = expires.format("%Y-%m-%d").to_string();
556 let status = if days_until < 30 {
557 self.error(&format!("{} (expires in {} days!)", expiry_str, days_until))
558 } else if days_until < 90 {
559 self.warning(&format!("{} ({} days)", expiry_str, days_until))
560 } else {
561 self.value(&format!("{} ({} days)", expiry_str, days_until))
562 };
563 output.push(format!(" {}: {}", self.label("Expires"), status));
564 }
565
566 if let Some(updated) = response.last_updated() {
567 output.push(format!(
568 " {}: {}",
569 self.label("Updated"),
570 self.value(&updated.format("%Y-%m-%d").to_string())
571 ));
572 }
573
574 if !response.status.is_empty() {
575 output.push(format!(" {}:", self.label("Status")));
576 for status in &response.status {
577 output.push(format!(" - {}", self.value(&sanitize_display(status))));
578 }
579 }
580
581 let nameservers = response.nameserver_names();
582 if !nameservers.is_empty() {
583 output.push(format!(" {}:", self.label("Nameservers")));
584 for ns in &nameservers {
585 output.push(format!(" - {}", self.value(&sanitize_display(ns))));
586 }
587 }
588
589 if response.is_dnssec_signed() {
590 output.push(format!(
591 " {}: {}",
592 self.label("DNSSEC"),
593 self.success("signed")
594 ));
595 }
596
597 if let Some(ref start) = response.start_address {
599 output.push(format!(
600 " {}: {}",
601 self.label("Start Address"),
602 self.value(&sanitize_display(start))
603 ));
604 }
605
606 if let Some(ref end) = response.end_address {
607 output.push(format!(
608 " {}: {}",
609 self.label("End Address"),
610 self.value(&sanitize_display(end))
611 ));
612 }
613
614 if let Some(ref country) = response.country {
615 output.push(format!(
616 " {}: {}",
617 self.label("Country"),
618 self.value(&sanitize_display(country))
619 ));
620 }
621
622 if let Some(start) = response.start_autnum {
624 output.push(format!(
625 " {}: {}",
626 self.label("AS Number"),
627 self.value(&format!(
628 "AS{} - AS{}",
629 start,
630 response.end_autnum.unwrap_or(start)
631 ))
632 ));
633 }
634
635 output.join("\n")
636 }
637
638 fn format_dns(&self, records: &[DnsRecord]) -> String {
639 let mut output = Vec::new();
640
641 if records.is_empty() {
642 output.push(self.warning("No records found"));
643 return output.join("\n");
644 }
645
646 let domain = &records[0].name;
647 let record_type = &records[0].record_type;
648 output.push(self.header(&format!(
649 "DNS {} Records: {}",
650 record_type,
651 sanitize_display(domain)
652 )));
653
654 for record in records {
655 output.push(format!(
656 " {} {} {} {}",
657 self.value(&sanitize_display(&record.name)),
658 self.label(&format!("{}", record.ttl)),
659 self.label(&format!("{}", record.record_type)),
660 self.success(&sanitize_display(&record.data.to_string()))
661 ));
662 }
663
664 output.join("\n")
665 }
666
667 fn format_propagation(&self, result: &PropagationResult) -> String {
668 let mut output = Vec::new();
669
670 output.push(self.header(&format!(
671 "Propagation Check: {} {}",
672 result.domain, result.record_type
673 )));
674
675 let percentage = result.propagation_percentage;
677 let percentage_str = format!("{:.1}%", percentage);
678 let status = if percentage >= 100.0 {
679 self.success(&format!("✓ Fully propagated ({})", percentage_str))
680 } else if percentage >= 80.0 {
681 self.warning(&format!("◐ Mostly propagated ({})", percentage_str))
682 } else if percentage >= 50.0 {
683 self.warning(&format!("◑ Partially propagated ({})", percentage_str))
684 } else {
685 self.error(&format!("✗ Not propagated ({})", percentage_str))
686 };
687 output.push(format!(" {}", status));
688
689 output.push(format!(
690 " {}: {}/{}",
691 self.label("Servers responding"),
692 result.servers_responding,
693 result.servers_checked
694 ));
695
696 if !result.consensus_values.is_empty() {
698 output.push(format!(" {}:", self.label("Consensus values")));
699 for value in &result.consensus_values {
700 output.push(format!(" - {}", self.success(&sanitize_display(value))));
701 }
702 }
703
704 if !result.inconsistencies.is_empty() {
706 output.push(format!(" {}:", self.label("Inconsistencies")));
707 for inconsistency in &result.inconsistencies {
708 output.push(format!(
709 " - {}",
710 self.warning(&sanitize_display(inconsistency))
711 ));
712 }
713 }
714
715 let mut by_region: std::collections::HashMap<&str, Vec<_>> =
717 std::collections::HashMap::new();
718 for server_result in &result.results {
719 by_region
720 .entry(server_result.server.location.as_str())
721 .or_default()
722 .push(server_result);
723 }
724
725 let mut regions: Vec<_> = by_region.keys().cloned().collect();
727 regions.sort();
728
729 output.push(format!("\n {}:", self.label("Results by Region")));
730 for region in ®ions {
731 output.push(format!("\n {}:", self.label(region)));
732 if let Some(server_results) = by_region.get(region) {
733 for server_result in server_results {
734 let status_icon = if server_result.success { "✓" } else { "✗" };
735 let status_colored = if server_result.success {
736 self.success(status_icon)
737 } else {
738 self.error(status_icon)
739 };
740
741 let values = if server_result.success {
742 if server_result.records.is_empty() {
743 "NXDOMAIN".to_string()
744 } else {
745 server_result
746 .records
747 .iter()
748 .map(|r| sanitize_display(&r.format_short()))
749 .collect::<Vec<_>>()
750 .join(", ")
751 }
752 } else {
753 sanitize_display(server_result.error.as_deref().unwrap_or("Error"))
754 };
755
756 output.push(format!(
757 " {} {} ({}) - {} [{}ms]",
758 status_colored,
759 self.value(&server_result.server.name),
760 server_result.server.ip,
761 values,
762 server_result.response_time_ms
763 ));
764 }
765 }
766 }
767
768 output.join("\n")
769 }
770
771 fn format_lookup(&self, result: &LookupResult) -> String {
772 let mut output = Vec::new();
773
774 let domain = result
775 .domain_name()
776 .unwrap_or_else(|| "Unknown".to_string());
777 let source = match result {
778 LookupResult::Rdap { .. } => "RDAP",
779 LookupResult::Whois { .. } => "WHOIS",
780 LookupResult::Available { .. } => "availability",
781 };
782
783 output.push(self.header(&format!(
784 "Lookup: {} (via {})",
785 sanitize_display(&domain),
786 source
787 )));
788
789 match result {
790 LookupResult::Rdap {
791 data,
792 whois_fallback,
793 } => {
794 output.push(format!(
795 " {}: {}",
796 self.label("Source"),
797 self.success("RDAP (modern protocol)")
798 ));
799
800 if let Some(registrar) = data.get_registrar() {
801 output.push(format!(
802 " {}: {}",
803 self.label("Registrar"),
804 self.value(&sanitize_display(®istrar))
805 ));
806 }
807
808 if let Some(registrant) = data.get_registrant() {
809 output.push(format!(
810 " {}: {}",
811 self.label("Registrant"),
812 self.value(&sanitize_display(®istrant))
813 ));
814 }
815
816 if let Some(organization) = data.get_registrant_organization() {
817 output.push(format!(
818 " {}: {}",
819 self.label("Organization"),
820 self.value(&sanitize_display(&organization))
821 ));
822 }
823
824 if let Some(contact) = data.get_registrant_contact() {
826 if contact.has_info() {
827 output.push(format!("\n {}:", self.label("Registrant Contact")));
828 if let Some(ref email) = contact.email {
829 output.push(format!(
830 " {}: {}",
831 self.label("Email"),
832 self.value(&sanitize_display(email))
833 ));
834 }
835 if let Some(ref phone) = contact.phone {
836 output.push(format!(
837 " {}: {}",
838 self.label("Phone"),
839 self.value(&sanitize_display(phone))
840 ));
841 }
842 if let Some(ref address) = contact.address {
843 output.push(format!(
844 " {}: {}",
845 self.label("Address"),
846 self.value(&sanitize_display(address))
847 ));
848 }
849 if let Some(ref country) = contact.country {
850 output.push(format!(
851 " {}: {}",
852 self.label("Country"),
853 self.value(&sanitize_display(country))
854 ));
855 }
856 }
857 }
858
859 if let Some(contact) = data.get_admin_contact() {
861 if contact.has_info() {
862 output.push(format!("\n {}:", self.label("Admin Contact")));
863 if let Some(ref name) = contact.name {
864 output.push(format!(
865 " {}: {}",
866 self.label("Name"),
867 self.value(&sanitize_display(name))
868 ));
869 }
870 if let Some(ref org) = contact.organization {
871 output.push(format!(
872 " {}: {}",
873 self.label("Organization"),
874 self.value(&sanitize_display(org))
875 ));
876 }
877 if let Some(ref email) = contact.email {
878 output.push(format!(
879 " {}: {}",
880 self.label("Email"),
881 self.value(&sanitize_display(email))
882 ));
883 }
884 if let Some(ref phone) = contact.phone {
885 output.push(format!(
886 " {}: {}",
887 self.label("Phone"),
888 self.value(&sanitize_display(phone))
889 ));
890 }
891 }
892 }
893
894 if let Some(contact) = data.get_tech_contact() {
896 if contact.has_info() {
897 output.push(format!("\n {}:", self.label("Tech Contact")));
898 if let Some(ref name) = contact.name {
899 output.push(format!(
900 " {}: {}",
901 self.label("Name"),
902 self.value(&sanitize_display(name))
903 ));
904 }
905 if let Some(ref org) = contact.organization {
906 output.push(format!(
907 " {}: {}",
908 self.label("Organization"),
909 self.value(&sanitize_display(org))
910 ));
911 }
912 if let Some(ref email) = contact.email {
913 output.push(format!(
914 " {}: {}",
915 self.label("Email"),
916 self.value(&sanitize_display(email))
917 ));
918 }
919 if let Some(ref phone) = contact.phone {
920 output.push(format!(
921 " {}: {}",
922 self.label("Phone"),
923 self.value(&sanitize_display(phone))
924 ));
925 }
926 }
927 }
928
929 if let Some(created) = data.creation_date() {
930 output.push(format!(
931 " {}: {}",
932 self.label("Created"),
933 self.value(&created.format("%Y-%m-%d").to_string())
934 ));
935 }
936
937 if let Some(expires) = data.expiration_date() {
938 let days_until = (expires - chrono::Utc::now()).num_days();
939 let expiry_str = expires.format("%Y-%m-%d").to_string();
940 let status = if days_until < 30 {
941 self.error(&format!("{} (expires in {} days!)", expiry_str, days_until))
942 } else if days_until < 90 {
943 self.warning(&format!("{} ({} days)", expiry_str, days_until))
944 } else {
945 self.value(&format!("{} ({} days)", expiry_str, days_until))
946 };
947 output.push(format!(" {}: {}", self.label("Expires"), status));
948 }
949
950 if !data.status.is_empty() {
951 output.push(format!(" {}:", self.label("Status")));
952 for status in &data.status {
953 output.push(format!(" - {}", self.value(&sanitize_display(status))));
954 }
955 }
956
957 let nameservers = data.nameserver_names();
958 if !nameservers.is_empty() {
959 output.push(format!(" {}:", self.label("Nameservers")));
960 for ns in &nameservers {
961 output.push(format!(" - {}", self.value(&sanitize_display(ns))));
962 }
963 }
964
965 if data.is_dnssec_signed() {
966 output.push(format!(
967 " {}: {}",
968 self.label("DNSSEC"),
969 self.success("signed")
970 ));
971 }
972
973 if let Some(whois) = whois_fallback {
974 let mut extra = Vec::new();
975
976 if data.get_registrant().is_none() {
978 if let Some(ref registrant) = whois.registrant {
979 extra.push(format!(
980 " {}: {}",
981 self.label("Registrant"),
982 self.value(&sanitize_display(registrant))
983 ));
984 }
985 }
986
987 if data.get_registrant_organization().is_none() {
989 if let Some(ref org) = whois.organization {
990 extra.push(format!(
991 " {}: {}",
992 self.label("Organization"),
993 self.value(&sanitize_display(org))
994 ));
995 }
996 }
997
998 let rdap_registrant = data.get_registrant_contact();
1000 let rdap_has_registrant =
1001 rdap_registrant.as_ref().is_some_and(|c| c.has_info());
1002 if !rdap_has_registrant {
1003 let has_whois_contact = whois.registrant_email.is_some()
1004 || whois.registrant_phone.is_some()
1005 || whois.registrant_address.is_some()
1006 || whois.registrant_country.is_some();
1007 if has_whois_contact {
1008 extra.push(format!("\n {}:", self.label("Registrant Contact")));
1009 if let Some(ref email) = whois.registrant_email {
1010 extra.push(format!(
1011 " {}: {}",
1012 self.label("Email"),
1013 self.value(&sanitize_display(email))
1014 ));
1015 }
1016 if let Some(ref phone) = whois.registrant_phone {
1017 extra.push(format!(
1018 " {}: {}",
1019 self.label("Phone"),
1020 self.value(&sanitize_display(phone))
1021 ));
1022 }
1023 if let Some(ref address) = whois.registrant_address {
1024 extra.push(format!(
1025 " {}: {}",
1026 self.label("Address"),
1027 self.value(&sanitize_display(address))
1028 ));
1029 }
1030 if let Some(ref country) = whois.registrant_country {
1031 extra.push(format!(
1032 " {}: {}",
1033 self.label("Country"),
1034 self.value(&sanitize_display(country))
1035 ));
1036 }
1037 }
1038 }
1039
1040 let rdap_has_admin = data.get_admin_contact().is_some_and(|c| c.has_info());
1042 if !rdap_has_admin {
1043 let has_whois_admin = whois.admin_name.is_some()
1044 || whois.admin_email.is_some()
1045 || whois.admin_phone.is_some();
1046 if has_whois_admin {
1047 extra.push(format!("\n {}:", self.label("Admin Contact")));
1048 if let Some(ref name) = whois.admin_name {
1049 extra.push(format!(
1050 " {}: {}",
1051 self.label("Name"),
1052 self.value(&sanitize_display(name))
1053 ));
1054 }
1055 if let Some(ref org) = whois.admin_organization {
1056 extra.push(format!(
1057 " {}: {}",
1058 self.label("Organization"),
1059 self.value(&sanitize_display(org))
1060 ));
1061 }
1062 if let Some(ref email) = whois.admin_email {
1063 extra.push(format!(
1064 " {}: {}",
1065 self.label("Email"),
1066 self.value(&sanitize_display(email))
1067 ));
1068 }
1069 if let Some(ref phone) = whois.admin_phone {
1070 extra.push(format!(
1071 " {}: {}",
1072 self.label("Phone"),
1073 self.value(&sanitize_display(phone))
1074 ));
1075 }
1076 }
1077 }
1078
1079 let rdap_has_tech = data.get_tech_contact().is_some_and(|c| c.has_info());
1081 if !rdap_has_tech {
1082 let has_whois_tech = whois.tech_name.is_some()
1083 || whois.tech_email.is_some()
1084 || whois.tech_phone.is_some();
1085 if has_whois_tech {
1086 extra.push(format!("\n {}:", self.label("Tech Contact")));
1087 if let Some(ref name) = whois.tech_name {
1088 extra.push(format!(
1089 " {}: {}",
1090 self.label("Name"),
1091 self.value(&sanitize_display(name))
1092 ));
1093 }
1094 if let Some(ref org) = whois.tech_organization {
1095 extra.push(format!(
1096 " {}: {}",
1097 self.label("Organization"),
1098 self.value(&sanitize_display(org))
1099 ));
1100 }
1101 if let Some(ref email) = whois.tech_email {
1102 extra.push(format!(
1103 " {}: {}",
1104 self.label("Email"),
1105 self.value(&sanitize_display(email))
1106 ));
1107 }
1108 if let Some(ref phone) = whois.tech_phone {
1109 extra.push(format!(
1110 " {}: {}",
1111 self.label("Phone"),
1112 self.value(&sanitize_display(phone))
1113 ));
1114 }
1115 }
1116 }
1117
1118 if let Some(updated) = whois.updated_date {
1120 extra.push(format!(
1121 " {}: {}",
1122 self.label("Updated"),
1123 self.value(&updated.format("%Y-%m-%d").to_string())
1124 ));
1125 }
1126
1127 if !data.is_dnssec_signed() {
1129 if let Some(ref dnssec) = whois.dnssec {
1130 extra.push(format!(
1131 " {}: {}",
1132 self.label("DNSSEC"),
1133 self.value(&sanitize_display(dnssec))
1134 ));
1135 }
1136 }
1137
1138 if !whois.whois_server.is_empty() {
1140 extra.push(format!(
1141 " {}: {}",
1142 self.label("WHOIS Server"),
1143 self.value(&sanitize_display(&whois.whois_server))
1144 ));
1145 }
1146
1147 if !extra.is_empty() {
1148 output.push(format!("\n {}", self.label("Additional WHOIS data:")));
1149 output.extend(extra);
1150 }
1151 }
1152 }
1153 LookupResult::Whois {
1154 data, rdap_error, ..
1155 } => {
1156 let source_note = if rdap_error.is_some() {
1157 "WHOIS (RDAP unavailable)"
1158 } else {
1159 "WHOIS"
1160 };
1161 output.push(format!(
1162 " {}: {}",
1163 self.label("Source"),
1164 self.warning(source_note)
1165 ));
1166
1167 if let Some(ref error) = rdap_error {
1168 output.push(format!(
1169 " {}: {}",
1170 self.label("RDAP Error"),
1171 self.error(error)
1172 ));
1173 }
1174
1175 if let Some(ref registrar) = data.registrar {
1176 output.push(format!(
1177 " {}: {}",
1178 self.label("Registrar"),
1179 self.value(&sanitize_display(registrar))
1180 ));
1181 }
1182
1183 if let Some(ref registrant) = data.registrant {
1184 output.push(format!(
1185 " {}: {}",
1186 self.label("Registrant"),
1187 self.value(&sanitize_display(registrant))
1188 ));
1189 }
1190
1191 if let Some(ref organization) = data.organization {
1192 output.push(format!(
1193 " {}: {}",
1194 self.label("Organization"),
1195 self.value(&sanitize_display(organization))
1196 ));
1197 }
1198
1199 let has_registrant_details = data.registrant_email.is_some()
1201 || data.registrant_phone.is_some()
1202 || data.registrant_address.is_some()
1203 || data.registrant_country.is_some();
1204
1205 if has_registrant_details {
1206 output.push(format!("\n {}:", self.label("Registrant Contact")));
1207 if let Some(ref email) = data.registrant_email {
1208 output.push(format!(
1209 " {}: {}",
1210 self.label("Email"),
1211 self.value(&sanitize_display(email))
1212 ));
1213 }
1214 if let Some(ref phone) = data.registrant_phone {
1215 output.push(format!(
1216 " {}: {}",
1217 self.label("Phone"),
1218 self.value(&sanitize_display(phone))
1219 ));
1220 }
1221 if let Some(ref address) = data.registrant_address {
1222 output.push(format!(
1223 " {}: {}",
1224 self.label("Address"),
1225 self.value(&sanitize_display(address))
1226 ));
1227 }
1228 if let Some(ref country) = data.registrant_country {
1229 output.push(format!(
1230 " {}: {}",
1231 self.label("Country"),
1232 self.value(&sanitize_display(country))
1233 ));
1234 }
1235 }
1236
1237 let has_admin_contact = data.admin_name.is_some()
1239 || data.admin_organization.is_some()
1240 || data.admin_email.is_some()
1241 || data.admin_phone.is_some();
1242
1243 if has_admin_contact {
1244 output.push(format!("\n {}:", self.label("Admin Contact")));
1245 if let Some(ref name) = data.admin_name {
1246 output.push(format!(
1247 " {}: {}",
1248 self.label("Name"),
1249 self.value(&sanitize_display(name))
1250 ));
1251 }
1252 if let Some(ref org) = data.admin_organization {
1253 output.push(format!(
1254 " {}: {}",
1255 self.label("Organization"),
1256 self.value(&sanitize_display(org))
1257 ));
1258 }
1259 if let Some(ref email) = data.admin_email {
1260 output.push(format!(
1261 " {}: {}",
1262 self.label("Email"),
1263 self.value(&sanitize_display(email))
1264 ));
1265 }
1266 if let Some(ref phone) = data.admin_phone {
1267 output.push(format!(
1268 " {}: {}",
1269 self.label("Phone"),
1270 self.value(&sanitize_display(phone))
1271 ));
1272 }
1273 }
1274
1275 let has_tech_contact = data.tech_name.is_some()
1277 || data.tech_organization.is_some()
1278 || data.tech_email.is_some()
1279 || data.tech_phone.is_some();
1280
1281 if has_tech_contact {
1282 output.push(format!("\n {}:", self.label("Tech Contact")));
1283 if let Some(ref name) = data.tech_name {
1284 output.push(format!(
1285 " {}: {}",
1286 self.label("Name"),
1287 self.value(&sanitize_display(name))
1288 ));
1289 }
1290 if let Some(ref org) = data.tech_organization {
1291 output.push(format!(
1292 " {}: {}",
1293 self.label("Organization"),
1294 self.value(&sanitize_display(org))
1295 ));
1296 }
1297 if let Some(ref email) = data.tech_email {
1298 output.push(format!(
1299 " {}: {}",
1300 self.label("Email"),
1301 self.value(&sanitize_display(email))
1302 ));
1303 }
1304 if let Some(ref phone) = data.tech_phone {
1305 output.push(format!(
1306 " {}: {}",
1307 self.label("Phone"),
1308 self.value(&sanitize_display(phone))
1309 ));
1310 }
1311 }
1312
1313 if let Some(created) = data.creation_date {
1314 output.push(format!(
1315 " {}: {}",
1316 self.label("Created"),
1317 self.value(&created.format("%Y-%m-%d").to_string())
1318 ));
1319 }
1320
1321 if let Some(expires) = data.expiration_date {
1322 let days_until = (expires - chrono::Utc::now()).num_days();
1323 let expiry_str = expires.format("%Y-%m-%d").to_string();
1324 let status = if days_until < 30 {
1325 self.error(&format!("{} (expires in {} days!)", expiry_str, days_until))
1326 } else if days_until < 90 {
1327 self.warning(&format!("{} ({} days)", expiry_str, days_until))
1328 } else {
1329 self.value(&format!("{} ({} days)", expiry_str, days_until))
1330 };
1331 output.push(format!(" {}: {}", self.label("Expires"), status));
1332 }
1333
1334 if !data.status.is_empty() {
1335 output.push(format!(" {}:", self.label("Status")));
1336 for status in &data.status {
1337 output.push(format!(" - {}", self.value(&sanitize_display(status))));
1338 }
1339 }
1340
1341 if !data.nameservers.is_empty() {
1342 output.push(format!(" {}:", self.label("Nameservers")));
1343 for ns in &data.nameservers {
1344 output.push(format!(" - {}", self.value(&sanitize_display(ns))));
1345 }
1346 }
1347
1348 if let Some(ref dnssec) = data.dnssec {
1349 output.push(format!(
1350 " {}: {}",
1351 self.label("DNSSEC"),
1352 self.value(&sanitize_display(dnssec))
1353 ));
1354 }
1355 }
1356 LookupResult::Available {
1357 data,
1358 rdap_error,
1359 whois_error,
1360 } => {
1361 output.push(format!(
1362 " {}: {}",
1363 self.label("Source"),
1364 self.warning("availability check (RDAP and WHOIS failed)")
1365 ));
1366
1367 let avail_str = if data.available {
1368 self.success("AVAILABLE")
1369 } else {
1370 self.error("TAKEN")
1371 };
1372 output.push(format!(" {}: {}", self.label("Availability"), avail_str));
1373
1374 let confidence_colored = match data.confidence.as_str() {
1375 "high" => self.success(&data.confidence),
1376 "medium" => self.warning(&data.confidence),
1377 _ => self.error(&data.confidence),
1378 };
1379 output.push(format!(
1380 " {}: {}",
1381 self.label("Confidence"),
1382 confidence_colored
1383 ));
1384 output.push(format!(
1385 " {}: {}",
1386 self.label("Method"),
1387 self.value(&data.method)
1388 ));
1389 if let Some(ref details) = data.details {
1390 output.push(format!(
1391 " {}: {}",
1392 self.label("Details"),
1393 self.value(details)
1394 ));
1395 }
1396 output.push(format!(
1397 " {}: {}",
1398 self.label("RDAP Error"),
1399 self.error(rdap_error)
1400 ));
1401 output.push(format!(
1402 " {}: {}",
1403 self.label("WHOIS Error"),
1404 self.error(whois_error)
1405 ));
1406 }
1407 }
1408
1409 output.join("\n")
1410 }
1411
1412 fn format_status(&self, response: &StatusResponse) -> String {
1413 let mut output = Vec::new();
1414
1415 output.push(self.header(&format!("Status: {}", sanitize_display(&response.domain))));
1416
1417 if let Some(status) = response.http_status {
1419 let status_text =
1420 sanitize_display(response.http_status_text.as_deref().unwrap_or("Unknown"));
1421 let status_display = if (200..300).contains(&status) {
1422 self.success(&format!("{} ({})", status, status_text))
1423 } else if (300..400).contains(&status) {
1424 self.warning(&format!("{} ({})", status, status_text))
1425 } else {
1426 self.error(&format!("{} ({})", status, status_text))
1427 };
1428 output.push(format!(
1429 " {}: {}",
1430 self.label("HTTP Status"),
1431 status_display
1432 ));
1433 }
1434
1435 if let Some(ref title) = response.title {
1437 output.push(format!(
1438 " {}: {}",
1439 self.label("Site Title"),
1440 self.value(&sanitize_display(title))
1441 ));
1442 }
1443
1444 if let Some(ref cert) = response.certificate {
1446 output.push(format!("\n {}:", self.label("SSL Certificate")));
1447 output.push(format!(
1448 " {}: {}",
1449 self.label("Subject"),
1450 self.value(&sanitize_display(&cert.subject))
1451 ));
1452 output.push(format!(
1453 " {}: {}",
1454 self.label("Issuer"),
1455 self.value(&sanitize_display(&cert.issuer))
1456 ));
1457
1458 let valid_status = if cert.is_valid {
1459 self.success("Valid")
1460 } else {
1461 self.error("Invalid")
1462 };
1463 output.push(format!(" {}: {}", self.label("Status"), valid_status));
1464
1465 output.push(format!(
1466 " {}: {}",
1467 self.label("Valid From"),
1468 self.value(&cert.valid_from.format("%Y-%m-%d").to_string())
1469 ));
1470
1471 let expiry_str = cert.valid_until.format("%Y-%m-%d").to_string();
1472 let expiry_display = if cert.days_until_expiry < 30 {
1473 self.error(&format!(
1474 "{} ({} days!)",
1475 expiry_str, cert.days_until_expiry
1476 ))
1477 } else if cert.days_until_expiry < 90 {
1478 self.warning(&format!("{} ({} days)", expiry_str, cert.days_until_expiry))
1479 } else {
1480 self.value(&format!("{} ({} days)", expiry_str, cert.days_until_expiry))
1481 };
1482 output.push(format!(" {}: {}", self.label("Expires"), expiry_display));
1483 } else {
1484 output.push(format!(
1485 "\n {}: {}",
1486 self.label("SSL Certificate"),
1487 self.warning("Not available (HTTPS may not be configured)")
1488 ));
1489 }
1490
1491 if let Some(ref expiry) = response.domain_expiration {
1493 output.push(format!("\n {}:", self.label("Domain Registration")));
1494
1495 if let Some(ref registrar) = expiry.registrar {
1496 output.push(format!(
1497 " {}: {}",
1498 self.label("Registrar"),
1499 self.value(&sanitize_display(registrar))
1500 ));
1501 }
1502
1503 let expiry_str = expiry.expiration_date.format("%Y-%m-%d").to_string();
1504 let expiry_display = if expiry.days_until_expiry < 30 {
1505 self.error(&format!(
1506 "{} ({} days!)",
1507 expiry_str, expiry.days_until_expiry
1508 ))
1509 } else if expiry.days_until_expiry < 90 {
1510 self.warning(&format!(
1511 "{} ({} days)",
1512 expiry_str, expiry.days_until_expiry
1513 ))
1514 } else {
1515 self.value(&format!(
1516 "{} ({} days)",
1517 expiry_str, expiry.days_until_expiry
1518 ))
1519 };
1520 output.push(format!(" {}: {}", self.label("Expires"), expiry_display));
1521 }
1522
1523 if let Some(ref dns) = response.dns_resolution {
1525 output.push(format!("\n {}:", self.label("DNS Resolution")));
1526
1527 if dns.resolves {
1529 output.push(format!(" {}", self.success("✓ Resolving")));
1530 } else {
1531 output.push(format!(" {}", self.error("✗ Domain does not resolve")));
1532 }
1533
1534 if let Some(ref cname) = dns.cname_target {
1536 output.push(format!(
1537 " {}: Aliases to {}",
1538 self.label("CNAME"),
1539 self.success(&sanitize_display(cname))
1540 ));
1541 }
1542
1543 if !dns.a_records.is_empty() {
1545 output.push(format!(" {}:", self.label("IPv4 (A)")));
1546 for ip in &dns.a_records {
1547 output.push(format!(" • {}", self.value(&sanitize_display(ip))));
1548 }
1549 }
1550
1551 if !dns.aaaa_records.is_empty() {
1553 output.push(format!(" {}:", self.label("IPv6 (AAAA)")));
1554 for ip in &dns.aaaa_records {
1555 output.push(format!(" • {}", self.value(&sanitize_display(ip))));
1556 }
1557 }
1558
1559 if !dns.nameservers.is_empty() {
1561 output.push(format!(" {}:", self.label("Nameservers")));
1562 for ns in &dns.nameservers {
1563 output.push(format!(" • {}", self.value(&sanitize_display(ns))));
1564 }
1565 }
1566 } else {
1567 output.push(format!(
1568 "\n {}: {}",
1569 self.label("DNS Resolution"),
1570 self.warning("Check failed")
1571 ));
1572 }
1573
1574 output.join("\n")
1575 }
1576
1577 fn format_follow_iteration(&self, iteration: &FollowIteration) -> String {
1578 let mut output = Vec::new();
1579
1580 let time_str = iteration.timestamp.format("%H:%M:%S").to_string();
1581 let iter_str = format!(
1582 "Iteration {}/{}",
1583 iteration.iteration, iteration.total_iterations
1584 );
1585
1586 if let Some(ref error) = iteration.error {
1587 output.push(format!(
1588 "[{}] {}: {}",
1589 self.label(&time_str),
1590 iter_str,
1591 self.error(error)
1592 ));
1593 return output.join("\n");
1594 }
1595
1596 let record_count = iteration.record_count();
1597 let status = if iteration.iteration == 1 {
1598 "".to_string()
1599 } else if iteration.changed {
1600 format!(" ({})", self.warning("CHANGED"))
1601 } else {
1602 format!(" ({})", self.success("unchanged"))
1603 };
1604
1605 let values: Vec<String> = iteration
1607 .records
1608 .iter()
1609 .map(|r| r.data.to_string().trim_end_matches('.').to_string())
1610 .collect();
1611
1612 output.push(format!(
1613 "[{}] {}: {} record(s){}",
1614 self.label(&time_str),
1615 iter_str,
1616 record_count,
1617 status
1618 ));
1619
1620 if !values.is_empty() {
1622 output.push(format!(" {}", self.value(&values.join(", "))));
1623 }
1624
1625 if !iteration.added.is_empty() {
1627 for added in &iteration.added {
1628 let value = added.trim_end_matches('.');
1629 output.push(format!(" {} {}", self.success("+"), self.success(value)));
1630 }
1631 }
1632 if !iteration.removed.is_empty() {
1633 for removed in &iteration.removed {
1634 let value = removed.trim_end_matches('.');
1635 output.push(format!(" {} {}", self.error("-"), self.error(value)));
1636 }
1637 }
1638
1639 output.join("\n")
1640 }
1641
1642 fn format_follow(&self, result: &FollowResult) -> String {
1643 let mut output = Vec::new();
1644
1645 output.push(self.header(&format!(
1646 "DNS Follow Complete: {} {}",
1647 result.domain, result.record_type
1648 )));
1649
1650 output.push(format!(
1652 " {}: {}/{}",
1653 self.label("Iterations completed"),
1654 result.completed_iterations(),
1655 result.iterations_requested
1656 ));
1657
1658 if result.interrupted {
1659 output.push(format!(
1660 " {}: {}",
1661 self.label("Status"),
1662 self.warning("Interrupted")
1663 ));
1664 }
1665
1666 output.push(format!(
1667 " {}: {}",
1668 self.label("Total changes detected"),
1669 if result.total_changes > 0 {
1670 self.warning(&result.total_changes.to_string())
1671 } else {
1672 self.success(&result.total_changes.to_string())
1673 }
1674 ));
1675
1676 let duration = result.ended_at - result.started_at;
1677 output.push(format!(
1678 " {}: {}",
1679 self.label("Duration"),
1680 self.value(&format_duration(duration))
1681 ));
1682
1683 if !result.iterations.is_empty() {
1685 output.push(format!("\n {}:", self.label("Iteration Details")));
1686 for iteration in &result.iterations {
1687 let time_str = iteration.timestamp.format("%H:%M:%S").to_string();
1688 let status = if iteration.error.is_some() {
1689 self.error("ERROR")
1690 } else if iteration.changed {
1691 self.warning("CHANGED")
1692 } else if iteration.iteration == 1 {
1693 self.value("initial")
1694 } else {
1695 self.success("stable")
1696 };
1697
1698 output.push(format!(
1699 " [{}] #{}: {} record(s) - {}",
1700 time_str,
1701 iteration.iteration,
1702 iteration.record_count(),
1703 status
1704 ));
1705 }
1706 }
1707
1708 output.join("\n")
1709 }
1710
1711 fn format_availability(&self, result: &crate::availability::AvailabilityResult) -> String {
1712 let mut output = Vec::new();
1713
1714 let status = if result.available {
1715 self.success("AVAILABLE")
1716 } else {
1717 self.error("TAKEN")
1718 };
1719 output.push(format!("{}: {}", sanitize_display(&result.domain), status));
1720 let confidence_colored = match result.confidence.as_str() {
1721 "high" => self.success(&result.confidence),
1722 "medium" => self.warning(&result.confidence),
1723 _ => self.error(&result.confidence),
1724 };
1725 output.push(format!(
1726 " {}: {}",
1727 self.label("Confidence"),
1728 confidence_colored
1729 ));
1730 output.push(format!(
1731 " {}: {}",
1732 self.label("Method"),
1733 self.value(&result.method)
1734 ));
1735 if let Some(ref details) = result.details {
1736 output.push(format!(
1737 " {}: {}",
1738 self.label("Details"),
1739 self.value(details)
1740 ));
1741 }
1742
1743 output.join("\n")
1744 }
1745
1746 fn format_dnssec(&self, report: &crate::dns::DnssecReport) -> String {
1747 let mut output = Vec::new();
1748
1749 output.push(format!(
1750 "DNSSEC Report for {}",
1751 self.success(&sanitize_display(&report.domain))
1752 ));
1753 output.push(String::new());
1754
1755 let status_colored = match report.status.as_str() {
1756 "secure" => self.success(&report.status),
1757 "insecure" | "partial" => self.warning(&report.status),
1758 _ => self.error(&report.status),
1759 };
1760 output.push(format!(" {}: {}", self.label("Status"), status_colored));
1761 let chain_colored = if report.chain_valid {
1762 self.success("valid")
1763 } else if report.has_ds_records && report.has_dnskey_records {
1764 self.error("invalid")
1765 } else {
1766 self.warning("n/a")
1767 };
1768 output.push(format!(
1769 " {}: {}",
1770 self.label("Chain Valid"),
1771 chain_colored
1772 ));
1773 output.push(format!(
1774 " {}: {}",
1775 self.label("Enabled"),
1776 self.value(&report.enabled.to_string())
1777 ));
1778 output.push(format!(
1779 " {}: {}",
1780 self.label("DS Records"),
1781 self.value(&report.ds_records.len().to_string())
1782 ));
1783 output.push(format!(
1784 " {}: {}",
1785 self.label("DNSKEY Records"),
1786 self.value(&report.dnskey_records.len().to_string())
1787 ));
1788
1789 if !report.ds_records.is_empty() {
1790 output.push(String::new());
1791 output.push(format!(" {}:", self.label("DS Records")));
1792 for ds in &report.ds_records {
1793 let match_indicator = if ds.matched_key && ds.digest_verified {
1794 self.success("\u{2713} verified")
1795 } else if ds.matched_key {
1796 self.error("\u{2717} digest mismatch")
1797 } else {
1798 self.error("\u{2717} no matching key")
1799 };
1800 output.push(format!(
1801 " Key Tag: {}, Algorithm: {} ({}), Digest: {} ({}) [{}]",
1802 ds.key_tag,
1803 ds.algorithm,
1804 sanitize_display(&ds.algorithm_name),
1805 ds.digest_type,
1806 sanitize_display(&ds.digest_type_name),
1807 match_indicator,
1808 ));
1809 }
1810 }
1811
1812 if !report.dnskey_records.is_empty() {
1813 output.push(String::new());
1814 output.push(format!(" {}:", self.label("DNSKEY Records")));
1815 for key in &report.dnskey_records {
1816 let role = if key.is_ksk {
1817 "KSK"
1818 } else if key.is_zsk {
1819 "ZSK"
1820 } else {
1821 "Other"
1822 };
1823 output.push(format!(
1824 " Key Tag: {}, Flags: {}, Role: {}, Algorithm: {} ({})",
1825 key.key_tag,
1826 key.flags,
1827 role,
1828 key.algorithm,
1829 sanitize_display(&key.algorithm_name)
1830 ));
1831 }
1832 }
1833
1834 if !report.issues.is_empty() {
1835 output.push(String::new());
1836 output.push(format!(" {}:", self.label("Issues")));
1837 for issue in &report.issues {
1838 output.push(format!(" - {}", sanitize_display(issue)));
1839 }
1840 }
1841
1842 output.join("\n")
1843 }
1844
1845 fn format_tld(&self, info: &crate::tld::TldInfo) -> String {
1846 let mut output = Vec::new();
1847
1848 output.push(self.header(&format!("TLD Info: .{}", info.tld)));
1849
1850 output.push(format!(
1851 " {}: {}",
1852 self.label("Type"),
1853 self.value(&info.tld_type)
1854 ));
1855
1856 if let Some(ref server) = info.whois_server {
1857 output.push(format!(
1858 " {}: {}",
1859 self.label("WHOIS Server"),
1860 self.value(server)
1861 ));
1862 } else {
1863 output.push(format!(
1864 " {}: {}",
1865 self.label("WHOIS Server"),
1866 self.warning("not available")
1867 ));
1868 }
1869
1870 if let Some(ref url) = info.rdap_url {
1871 output.push(format!(" {}: {}", self.label("RDAP URL"), self.value(url)));
1872 } else {
1873 output.push(format!(
1874 " {}: {}",
1875 self.label("RDAP URL"),
1876 self.warning("not available")
1877 ));
1878 }
1879
1880 if let Some(ref url) = info.registry_url {
1881 output.push(format!(" {}: {}", self.label("Registry"), self.value(url)));
1882 } else {
1883 output.push(format!(
1884 " {}: {}",
1885 self.label("Registry"),
1886 self.warning("not available")
1887 ));
1888 }
1889
1890 output.join("\n")
1891 }
1892
1893 fn format_dns_comparison(&self, comparison: &crate::dns::DnsComparison) -> String {
1894 let mut output = Vec::new();
1895
1896 output.push(self.header(&format!(
1897 "DNS Comparison: {} {}",
1898 comparison.domain, comparison.record_type
1899 )));
1900
1901 if comparison.matches {
1903 output.push(format!(" {} Records match", self.success("✓")));
1904 } else {
1905 output.push(format!(" {} Records differ", self.error("✗")));
1906 }
1907 output.push(String::new());
1908
1909 if let Some(ref err) = comparison.server_a.error {
1911 output.push(format!(
1912 " {} ({}): {}",
1913 self.label("Server A"),
1914 self.value(&sanitize_display(&comparison.server_a.nameserver)),
1915 self.error(&sanitize_display(err))
1916 ));
1917 } else {
1918 output.push(format!(
1919 " {} ({}): {} records",
1920 self.label("Server A"),
1921 self.value(&sanitize_display(&comparison.server_a.nameserver)),
1922 self.value(&comparison.server_a.records.len().to_string())
1923 ));
1924 for record in &comparison.server_a.records {
1925 output.push(format!(
1926 " - {}",
1927 self.value(&sanitize_display(&record.format_short()))
1928 ));
1929 }
1930 }
1931 output.push(String::new());
1932
1933 if let Some(ref err) = comparison.server_b.error {
1935 output.push(format!(
1936 " {} ({}): {}",
1937 self.label("Server B"),
1938 self.value(&sanitize_display(&comparison.server_b.nameserver)),
1939 self.error(&sanitize_display(err))
1940 ));
1941 } else {
1942 output.push(format!(
1943 " {} ({}): {} records",
1944 self.label("Server B"),
1945 self.value(&sanitize_display(&comparison.server_b.nameserver)),
1946 self.value(&comparison.server_b.records.len().to_string())
1947 ));
1948 for record in &comparison.server_b.records {
1949 output.push(format!(
1950 " - {}",
1951 self.value(&sanitize_display(&record.format_short()))
1952 ));
1953 }
1954 }
1955 output.push(String::new());
1956
1957 output.push(format!(
1959 " {}: {}",
1960 self.label("Common"),
1961 if comparison.common.is_empty() {
1962 self.warning("(none)")
1963 } else {
1964 self.value(&sanitize_display(&comparison.common.join(", ")))
1965 }
1966 ));
1967
1968 output.push(format!(
1970 " {}: {}",
1971 self.label(&format!(
1972 "Only in {}",
1973 sanitize_display(&comparison.server_a.nameserver)
1974 )),
1975 if comparison.only_in_a.is_empty() {
1976 self.warning("(none)")
1977 } else {
1978 self.error(&sanitize_display(&comparison.only_in_a.join(", ")))
1979 }
1980 ));
1981
1982 output.push(format!(
1984 " {}: {}",
1985 self.label(&format!(
1986 "Only in {}",
1987 sanitize_display(&comparison.server_b.nameserver)
1988 )),
1989 if comparison.only_in_b.is_empty() {
1990 self.warning("(none)")
1991 } else {
1992 self.error(&sanitize_display(&comparison.only_in_b.join(", ")))
1993 }
1994 ));
1995
1996 output.join("\n")
1997 }
1998
1999 fn format_subdomains(&self, result: &crate::subdomains::SubdomainResult) -> String {
2000 let mut output = Vec::new();
2001
2002 output.push(self.header(&format!("Subdomains: {}", sanitize_display(&result.domain))));
2003
2004 output.push(format!(
2005 " {}: {}",
2006 self.label("Source"),
2007 self.value(&sanitize_display(&result.source))
2008 ));
2009 output.push(format!(
2010 " {}: {}",
2011 self.label("Count"),
2012 self.value(&result.count.to_string())
2013 ));
2014
2015 if result.subdomains.is_empty() {
2016 output.push(format!(" {}", self.warning("No subdomains found")));
2017 } else {
2018 output.push(String::new());
2019 for subdomain in &result.subdomains {
2020 output.push(format!(
2021 " - {}",
2022 self.value(&sanitize_display(subdomain))
2023 ));
2024 }
2025 }
2026
2027 output.join("\n")
2028 }
2029
2030 fn format_diff(&self, diff: &crate::diff::DomainDiff) -> String {
2031 let mut output = Vec::new();
2032
2033 output.push(self.header(&format!(
2034 "Diff: {} vs {}",
2035 sanitize_display(&diff.domain_a),
2036 sanitize_display(&diff.domain_b)
2037 )));
2038
2039 output.push(format!("\n {}:", self.label("Registration")));
2041 let reg = &diff.registration;
2042 output.push(format!(
2043 " {}: {} | {}",
2044 self.label("Registrar"),
2045 self.value(&sanitize_display(
2046 reg.registrar.0.as_deref().unwrap_or("N/A")
2047 )),
2048 self.value(&sanitize_display(
2049 reg.registrar.1.as_deref().unwrap_or("N/A")
2050 ))
2051 ));
2052 output.push(format!(
2053 " {}: {} | {}",
2054 self.label("Organization"),
2055 self.value(&sanitize_display(
2056 reg.organization.0.as_deref().unwrap_or("N/A")
2057 )),
2058 self.value(&sanitize_display(
2059 reg.organization.1.as_deref().unwrap_or("N/A")
2060 ))
2061 ));
2062 output.push(format!(
2063 " {}: {} | {}",
2064 self.label("Created"),
2065 self.value(reg.created.0.as_deref().unwrap_or("N/A")),
2066 self.value(reg.created.1.as_deref().unwrap_or("N/A"))
2067 ));
2068 output.push(format!(
2069 " {}: {} | {}",
2070 self.label("Expires"),
2071 self.value(reg.expires.0.as_deref().unwrap_or("N/A")),
2072 self.value(reg.expires.1.as_deref().unwrap_or("N/A"))
2073 ));
2074
2075 output.push(format!("\n {}:", self.label("DNS")));
2077 let dns = &diff.dns;
2078 {
2079 let (res_a, res_b) = dns.resolves;
2080 output.push(format!(
2081 " {}: {} | {}",
2082 self.label("Resolves"),
2083 if res_a {
2084 self.success("yes")
2085 } else {
2086 self.error("no")
2087 },
2088 if res_b {
2089 self.success("yes")
2090 } else {
2091 self.error("no")
2092 }
2093 ));
2094 }
2095 output.push(format!(
2096 " {}: {} | {}",
2097 self.label("A Records"),
2098 self.value(&sanitize_display(&dns.a_records.0.join(", "))),
2099 self.value(&sanitize_display(&dns.a_records.1.join(", ")))
2100 ));
2101 output.push(format!(
2102 " {}: {} | {}",
2103 self.label("Nameservers"),
2104 self.value(&sanitize_display(&dns.nameservers.0.join(", "))),
2105 self.value(&sanitize_display(&dns.nameservers.1.join(", ")))
2106 ));
2107
2108 output.push(format!("\n {}:", self.label("SSL")));
2110 let ssl = &diff.ssl;
2111 output.push(format!(
2112 " {}: {} | {}",
2113 self.label("Issuer"),
2114 self.value(&sanitize_display(ssl.issuer.0.as_deref().unwrap_or("N/A"))),
2115 self.value(&sanitize_display(ssl.issuer.1.as_deref().unwrap_or("N/A")))
2116 ));
2117 output.push(format!(
2118 " {}: {} | {}",
2119 self.label("Valid Until"),
2120 self.value(ssl.valid_until.0.as_deref().unwrap_or("N/A")),
2121 self.value(ssl.valid_until.1.as_deref().unwrap_or("N/A"))
2122 ));
2123 {
2124 let a_str = ssl.days_remaining.0.map(|d| d.to_string());
2125 let b_str = ssl.days_remaining.1.map(|d| d.to_string());
2126 output.push(format!(
2127 " {}: {} | {}",
2128 self.label("Days Remaining"),
2129 self.value(a_str.as_deref().unwrap_or("N/A")),
2130 self.value(b_str.as_deref().unwrap_or("N/A"))
2131 ));
2132 }
2133 {
2134 let a_str = ssl.is_valid.0.map(|v| if v { "yes" } else { "no" });
2135 let b_str = ssl.is_valid.1.map(|v| if v { "yes" } else { "no" });
2136 output.push(format!(
2137 " {}: {} | {}",
2138 self.label("Valid"),
2139 self.value(a_str.unwrap_or("N/A")),
2140 self.value(b_str.unwrap_or("N/A"))
2141 ));
2142 }
2143
2144 output.join("\n")
2145 }
2146
2147 fn format_ssl(&self, report: &crate::ssl::SslReport) -> String {
2148 let mut output = Vec::new();
2149
2150 output.push(self.header(&format!("SSL Report: {}", sanitize_display(&report.domain))));
2151
2152 output.push(format!(
2153 " {}: {}",
2154 self.label("Valid"),
2155 if report.is_valid {
2156 self.success("yes")
2157 } else {
2158 self.error("no")
2159 }
2160 ));
2161 output.push(format!(
2162 " {}: {}",
2163 self.label("Days Until Expiry"),
2164 self.value(&report.days_until_expiry.to_string())
2165 ));
2166
2167 if let Some(ref proto) = report.protocol_version {
2168 output.push(format!(
2169 " {}: {}",
2170 self.label("Protocol"),
2171 self.value(&sanitize_display(proto))
2172 ));
2173 }
2174
2175 if !report.san_names.is_empty() {
2176 let sanitized_sans: Vec<String> = report
2177 .san_names
2178 .iter()
2179 .map(|s| sanitize_display(s))
2180 .collect();
2181 output.push(format!(
2182 " {}: {}",
2183 self.label("SANs"),
2184 self.value(&sanitized_sans.join(", "))
2185 ));
2186 }
2187
2188 if !report.chain.is_empty() {
2189 output.push(String::new());
2190 output.push(format!(" {}:", self.label("Certificate Chain")));
2191 for (i, cert) in report.chain.iter().enumerate() {
2192 output.push(format!(
2193 " [{}] {}",
2194 i,
2195 self.value(&sanitize_display(&cert.subject))
2196 ));
2197 output.push(format!(
2198 " {}: {}",
2199 self.label("Issuer"),
2200 self.value(&sanitize_display(&cert.issuer))
2201 ));
2202 if let Some(ref alg) = cert.signature_algorithm {
2203 output.push(format!(
2204 " {}: {}",
2205 self.label("Algorithm"),
2206 self.value(&sanitize_display(alg))
2207 ));
2208 }
2209 if let Some(ref key_type) = cert.key_type {
2210 let key_info = if let Some(bits) = cert.key_bits {
2211 format!("{} ({} bits)", sanitize_display(key_type), bits)
2212 } else {
2213 sanitize_display(key_type)
2214 };
2215 output.push(format!(
2216 " {}: {}",
2217 self.label("Key"),
2218 self.value(&key_info)
2219 ));
2220 }
2221 output.push(format!(
2222 " {}: {} to {}",
2223 self.label("Validity"),
2224 self.value(&cert.valid_from.format("%Y-%m-%d").to_string()),
2225 self.value(&cert.valid_until.format("%Y-%m-%d").to_string())
2226 ));
2227 }
2228 }
2229
2230 output.join("\n")
2231 }
2232
2233 fn format_watch(&self, report: &crate::watchlist::WatchReport) -> String {
2234 let mut output = Vec::new();
2235
2236 output.push(self.header("Domain Watch Report"));
2237
2238 output.push(format!(
2239 " {}: {}",
2240 self.label("Checked"),
2241 self.value(
2242 &report
2243 .checked_at
2244 .format("%Y-%m-%d %H:%M:%S UTC")
2245 .to_string()
2246 )
2247 ));
2248 output.push(format!(
2249 " {}: {} domains, {} warnings",
2250 self.label("Total"),
2251 self.value(&report.total.to_string()),
2252 if report.warnings > 0 {
2253 self.warning(&report.warnings.to_string())
2254 } else {
2255 self.value(&report.warnings.to_string())
2256 }
2257 ));
2258
2259 for r in &report.results {
2260 output.push(String::new());
2261
2262 let icon = if r.issues.is_empty() {
2263 self.success("v")
2264 } else {
2265 self.warning("!")
2266 };
2267 output.push(format!(
2268 " {} {}",
2269 icon,
2270 self.value(&sanitize_display(&r.domain))
2271 ));
2272
2273 let ssl_str = r
2275 .ssl_days_remaining
2276 .map(|d| format!("{} days", d))
2277 .unwrap_or_else(|| "N/A".to_string());
2278 let dom_str = r
2279 .domain_days_remaining
2280 .map(|d| format!("{} days", d))
2281 .unwrap_or_else(|| "N/A".to_string());
2282 let http_str = r
2283 .http_status
2284 .map(|s| s.to_string())
2285 .unwrap_or_else(|| "N/A".to_string());
2286
2287 output.push(format!(
2288 " {}: {} | {}: {} | {}: {}",
2289 self.label("SSL"),
2290 self.value(&ssl_str),
2291 self.label("Domain"),
2292 self.value(&dom_str),
2293 self.label("HTTP"),
2294 self.value(&http_str)
2295 ));
2296
2297 if !r.issues.is_empty() {
2298 output.push(format!(" {}:", self.label("Issues")));
2299 for issue in &r.issues {
2300 output.push(format!(
2301 " - {}",
2302 self.warning(&sanitize_display(issue))
2303 ));
2304 }
2305 }
2306 }
2307
2308 output.join("\n")
2309 }
2310
2311 fn format_domain_info(&self, info: &crate::domain_info::DomainInfo) -> String {
2312 let mut output = Vec::new();
2313
2314 let source_str = match info.source {
2315 crate::domain_info::DomainInfoSource::Both => "both",
2316 crate::domain_info::DomainInfoSource::Rdap => "rdap",
2317 crate::domain_info::DomainInfoSource::Whois => "whois",
2318 crate::domain_info::DomainInfoSource::Available => "available",
2319 };
2320
2321 output.push(self.header(&format!(
2322 "Domain Info: {} (source: {})",
2323 sanitize_display(&info.domain),
2324 source_str
2325 )));
2326
2327 if let Some(ref registrar) = info.registrar {
2329 output.push(format!(
2330 " {}: {}",
2331 self.label("Registrar"),
2332 self.value(&sanitize_display(registrar))
2333 ));
2334 }
2335 if let Some(ref registrant) = info.registrant {
2336 output.push(format!(
2337 " {}: {}",
2338 self.label("Registrant"),
2339 self.value(&sanitize_display(registrant))
2340 ));
2341 }
2342 if let Some(ref organization) = info.organization {
2343 output.push(format!(
2344 " {}: {}",
2345 self.label("Organization"),
2346 self.value(&sanitize_display(organization))
2347 ));
2348 }
2349
2350 if let Some(ref created) = info.creation_date {
2352 output.push(format!(
2353 " {}: {}",
2354 self.label("Created"),
2355 self.value(&created.format("%Y-%m-%d").to_string())
2356 ));
2357 }
2358 if let Some(ref expires) = info.expiration_date {
2359 output.push(format!(
2360 " {}: {}",
2361 self.label("Expires"),
2362 self.value(&expires.format("%Y-%m-%d").to_string())
2363 ));
2364 }
2365 if let Some(ref updated) = info.updated_date {
2366 output.push(format!(
2367 " {}: {}",
2368 self.label("Updated"),
2369 self.value(&updated.format("%Y-%m-%d").to_string())
2370 ));
2371 }
2372
2373 if !info.nameservers.is_empty() {
2375 output.push(format!(
2376 " {}: {}",
2377 self.label("Nameservers"),
2378 self.value(&info.nameservers.join(", "))
2379 ));
2380 }
2381 if !info.status.is_empty() {
2382 output.push(format!(
2383 " {}: {}",
2384 self.label("Status"),
2385 self.value(&info.status.join(", "))
2386 ));
2387 }
2388 if let Some(ref dnssec) = info.dnssec {
2389 output.push(format!(
2390 " {}: {}",
2391 self.label("DNSSEC"),
2392 self.value(&sanitize_display(dnssec))
2393 ));
2394 }
2395
2396 let has_registrant_contact = info.registrant_email.is_some()
2398 || info.registrant_phone.is_some()
2399 || info.registrant_address.is_some()
2400 || info.registrant_country.is_some();
2401 if has_registrant_contact {
2402 output.push(format!("\n {}:", self.label("Registrant Contact")));
2403 if let Some(ref email) = info.registrant_email {
2404 output.push(format!(
2405 " {}: {}",
2406 self.label("Email"),
2407 self.value(&sanitize_display(email))
2408 ));
2409 }
2410 if let Some(ref phone) = info.registrant_phone {
2411 output.push(format!(
2412 " {}: {}",
2413 self.label("Phone"),
2414 self.value(&sanitize_display(phone))
2415 ));
2416 }
2417 if let Some(ref address) = info.registrant_address {
2418 output.push(format!(
2419 " {}: {}",
2420 self.label("Address"),
2421 self.value(&sanitize_display(address))
2422 ));
2423 }
2424 if let Some(ref country) = info.registrant_country {
2425 output.push(format!(
2426 " {}: {}",
2427 self.label("Country"),
2428 self.value(&sanitize_display(country))
2429 ));
2430 }
2431 }
2432
2433 let has_admin_contact = info.admin_name.is_some()
2435 || info.admin_organization.is_some()
2436 || info.admin_email.is_some()
2437 || info.admin_phone.is_some();
2438 if has_admin_contact {
2439 output.push(format!("\n {}:", self.label("Admin Contact")));
2440 if let Some(ref name) = info.admin_name {
2441 output.push(format!(
2442 " {}: {}",
2443 self.label("Name"),
2444 self.value(&sanitize_display(name))
2445 ));
2446 }
2447 if let Some(ref org) = info.admin_organization {
2448 output.push(format!(
2449 " {}: {}",
2450 self.label("Organization"),
2451 self.value(&sanitize_display(org))
2452 ));
2453 }
2454 if let Some(ref email) = info.admin_email {
2455 output.push(format!(
2456 " {}: {}",
2457 self.label("Email"),
2458 self.value(&sanitize_display(email))
2459 ));
2460 }
2461 if let Some(ref phone) = info.admin_phone {
2462 output.push(format!(
2463 " {}: {}",
2464 self.label("Phone"),
2465 self.value(&sanitize_display(phone))
2466 ));
2467 }
2468 }
2469
2470 let has_tech_contact = info.tech_name.is_some()
2472 || info.tech_organization.is_some()
2473 || info.tech_email.is_some()
2474 || info.tech_phone.is_some();
2475 if has_tech_contact {
2476 output.push(format!("\n {}:", self.label("Tech Contact")));
2477 if let Some(ref name) = info.tech_name {
2478 output.push(format!(
2479 " {}: {}",
2480 self.label("Name"),
2481 self.value(&sanitize_display(name))
2482 ));
2483 }
2484 if let Some(ref org) = info.tech_organization {
2485 output.push(format!(
2486 " {}: {}",
2487 self.label("Organization"),
2488 self.value(&sanitize_display(org))
2489 ));
2490 }
2491 if let Some(ref email) = info.tech_email {
2492 output.push(format!(
2493 " {}: {}",
2494 self.label("Email"),
2495 self.value(&sanitize_display(email))
2496 ));
2497 }
2498 if let Some(ref phone) = info.tech_phone {
2499 output.push(format!(
2500 " {}: {}",
2501 self.label("Phone"),
2502 self.value(&sanitize_display(phone))
2503 ));
2504 }
2505 }
2506
2507 let has_metadata = info.whois_server.is_some() || info.rdap_url.is_some();
2509 if has_metadata {
2510 output.push(format!("\n {}:", self.label("Protocol Metadata")));
2511 if let Some(ref whois_server) = info.whois_server {
2512 output.push(format!(
2513 " {}: {}",
2514 self.label("WHOIS Server"),
2515 self.value(&sanitize_display(whois_server))
2516 ));
2517 }
2518 if let Some(ref rdap_url) = info.rdap_url {
2519 output.push(format!(
2520 " {}: {}",
2521 self.label("RDAP URL"),
2522 self.value(&sanitize_display(rdap_url))
2523 ));
2524 }
2525 }
2526
2527 output.join("\n")
2528 }
2529}