@@ -3,6 +3,7 @@ package main
33import (
44 "bufio"
55 "context"
6+ "encoding/csv"
67 "encoding/json"
78 "flag"
89 "fmt"
@@ -67,12 +68,13 @@ var asnMap = map[string]string{
6768
6869func main () {
6970 var (
70- fileArg = flag .String ("f" , "" , "Path to file with one domain per line" )
71- workers = flag .Int ("workers" , 20 , "Number of concurrent workers for file input" )
72- timeout = flag .Duration ("timeout" , 2 * time .Second , "Per-lookup timeout" )
73- jsonOut = flag .Bool ("json" , false , "Output as JSON" )
74- mEgg = flag .Bool ("m" , false , "" ) // intentionally undocumented
75- author = flag .Bool ("a" , false , "Show author and repository details" )
71+ fileArg = flag .String ("f" , "" , "Path to file with one domain per line" )
72+ workers = flag .Int ("workers" , 20 , "Number of concurrent workers for file input" )
73+ timeout = flag .Duration ("timeout" , 2 * time .Second , "Per-lookup timeout" )
74+ jsonOut = flag .Bool ("json" , false , "Output as JSON" )
75+ outputArg = flag .String ("output" , "table" , "Output format: table, json, csv, html" )
76+ mEgg = flag .Bool ("m" , false , "" ) // intentionally undocumented
77+ author = flag .Bool ("a" , false , "Show author and repository details" )
7678 )
7779 flag .Parse ()
7880
@@ -98,7 +100,7 @@ func main() {
98100 domains = d
99101 } else {
100102 if flag .NArg () < 1 {
101- fmt .Fprintln (os .Stderr , "Usage: mail-lens [-f domains.txt] [--workers 20] [--json] <domain>" )
103+ fmt .Fprintln (os .Stderr , "Usage: mail-lens [-f domains.txt] [--workers 20] [--json] [--output table|json|csv|html] <domain>" )
102104 os .Exit (2 )
103105 }
104106 domain := normaliseDomain (flag .Arg (0 ))
@@ -123,14 +125,23 @@ func main() {
123125
124126 sort .Slice (results , func (i , j int ) bool { return results [i ].Domain < results [j ].Domain })
125127
128+ out := strings .ToLower (strings .TrimSpace (* outputArg ))
126129 if * jsonOut {
130+ out = "json"
131+ }
132+
133+ switch out {
134+ case "json" :
127135 enc := json .NewEncoder (os .Stdout )
128136 enc .SetIndent ("" , " " )
129137 _ = enc .Encode (results )
130- return
138+ case "csv" :
139+ emitCSV (results )
140+ case "html" :
141+ emitHTML (results )
142+ default :
143+ printTable (results )
131144 }
132-
133- printTable (results )
134145}
135146
136147func readDomainsFromFile (path string ) ([]string , error ) {
@@ -421,6 +432,53 @@ func printTable(results []Result) {
421432 _ = w .Flush ()
422433}
423434
435+ func emitCSV (results []Result ) {
436+ w := csv .NewWriter (os .Stdout )
437+ _ = w .Write ([]string {"domain" , "primary_mx" , "provider" , "security_stack" , "asn" , "organisation" , "status" })
438+ for _ , r := range results {
439+ status := "OK"
440+ if r .Error != "" {
441+ status = r .Error
442+ }
443+ stack := ""
444+ if len (r .SecurityStack ) > 0 {
445+ stack = strings .Join (r .SecurityStack , "," )
446+ }
447+ _ = w .Write ([]string {r .Domain , r .PrimaryMX , r .Provider , stack , r .ASN , r .Organisation , status })
448+ }
449+ w .Flush ()
450+ }
451+
452+ func emitHTML (results []Result ) {
453+ fmt .Println ("<!doctype html><html lang=\" en\" ><head><meta charset=\" utf-8\" ><title>mail-lens</title><style>body{font-family:Arial,sans-serif;margin:16px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ccc;padding:6px 8px;font-size:12px;text-align:left}th{background:#f2f2f2}</style></head><body>" )
454+ fmt .Println ("<h1>mail-lens report</h1><table><thead><tr><th>Domain</th><th>Primary MX</th><th>Provider</th><th>Security Stack</th><th>ASN</th><th>Organisation</th><th>Status</th></tr></thead><tbody>" )
455+ for _ , r := range results {
456+ status := "OK"
457+ if r .Error != "" {
458+ status = r .Error
459+ }
460+ stack := "-"
461+ if len (r .SecurityStack ) > 0 {
462+ stack = strings .Join (r .SecurityStack , "," )
463+ }
464+ fmt .Printf ("<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>\n " ,
465+ htmlEscape (empty (r .Domain )),
466+ htmlEscape (empty (r .PrimaryMX )),
467+ htmlEscape (empty (r .Provider )),
468+ htmlEscape (stack ),
469+ htmlEscape (empty (r .ASN )),
470+ htmlEscape (empty (r .Organisation )),
471+ htmlEscape (status ),
472+ )
473+ }
474+ fmt .Println ("</tbody></table></body></html>" )
475+ }
476+
477+ func htmlEscape (v string ) string {
478+ replacer := strings .NewReplacer ("&" , "&" , "<" , "<" , ">" , ">" , "\" " , """ , "'" , "'" )
479+ return replacer .Replace (v )
480+ }
481+
424482func empty (s string ) string {
425483 if strings .TrimSpace (s ) == "" {
426484 return "-"
0 commit comments