Skip to content

Commit 254ffc2

Browse files
committed
Add CSV and HTML output modes to mail-lens-go
1 parent 18409f6 commit 254ffc2

3 files changed

Lines changed: 68 additions & 10 deletions

File tree

tools/mail-lens-go/mail-lens

32 KB
Binary file not shown.

tools/mail-lens-go/mail-lens.exe

33.5 KB
Binary file not shown.

tools/mail-lens-go/main.go

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"bufio"
55
"context"
6+
"encoding/csv"
67
"encoding/json"
78
"flag"
89
"fmt"
@@ -67,12 +68,13 @@ var asnMap = map[string]string{
6768

6869
func 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

136147
func 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("&", "&amp;", "<", "&lt;", ">", "&gt;", "\"", "&quot;", "'", "&#39;")
479+
return replacer.Replace(v)
480+
}
481+
424482
func empty(s string) string {
425483
if strings.TrimSpace(s) == "" {
426484
return "-"

0 commit comments

Comments
 (0)