Skip to main content

seer_core/output/
human.rs

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
14/// Strips ANSI escape sequences from untrusted external strings to prevent
15/// terminal injection via malicious WHOIS/RDAP response data.
16static 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        // Registrant contact details
149        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        // Admin contact
187        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        // Tech contact
225        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(&registrar))
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(&registrant))
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        // Registrant contact details
364        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        // Admin contact
399        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        // Tech contact
448        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        // Billing contact
497        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        // IP-specific fields
598        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        // ASN-specific fields
623        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        // Summary
676        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        // Consensus values
697        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        // Inconsistencies
705        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        // Group results by region
716        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        // Sort regions for consistent output
726        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 &regions {
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(&registrar))
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(&registrant))
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                // Registrant contact details
825                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                // Admin contact
860                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                // Tech contact
895                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                    // Registrant (if RDAP didn't have it)
977                    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                    // Organization (if RDAP didn't have it)
988                    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                    // Registrant contact details (if RDAP didn't have them)
999                    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                    // Admin contact (if RDAP didn't have it)
1041                    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                    // Tech contact (if RDAP didn't have it)
1080                    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                    // Updated date (RDAP doesn't typically expose this)
1119                    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                    // DNSSEC (if RDAP didn't show it)
1128                    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                    // WHOIS server
1139                    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                // Registrant contact details
1200                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                // Admin contact
1238                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                // Tech contact
1276                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        // HTTP Status
1418        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        // Site Title
1436        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        // SSL Certificate
1445        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        // Domain Expiration
1492        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        // DNS Resolution
1524        if let Some(ref dns) = response.dns_resolution {
1525            output.push(format!("\n  {}:", self.label("DNS Resolution")));
1526
1527            // Status line
1528            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            // CNAME if present
1535            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            // IPv4 addresses (A records)
1544            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            // IPv6 addresses (AAAA records)
1552            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            // Nameservers
1560            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        // Collect record values, trimming trailing dots
1606        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        // Show records comma-separated on a single indented line
1621        if !values.is_empty() {
1622            output.push(format!("  {}", self.value(&values.join(", "))));
1623        }
1624
1625        // Show changes if any
1626        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        // Summary
1651        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        // Show iteration details
1684        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        // Match status
1902        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        // Server A
1910        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        // Server B
1934        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        // Common records
1958        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        // Only in A
1969        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        // Only in B
1983        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        // Registration
2040        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        // DNS
2076        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        // SSL
2109        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            // Condensed status line: SSL | Domain | HTTP
2274            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        // Registration
2328        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        // Dates
2351        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        // DNS
2374        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        // Registrant Contact
2397        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        // Admin Contact
2434        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        // Tech Contact
2471        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        // Protocol Metadata
2508        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}