checker-email-autoconfig/checker/report_template.go

269 lines
9.5 KiB
Go

package checker
import "html/template"
var templateFuncs = template.FuncMap{
"string": func(s reportStatus) string { return string(s) },
"badgeClass": func(s reportStatus) string {
switch s {
case statusOK:
return "badge-ok"
case statusWarn:
return "badge-warn"
case statusFail:
return "badge-fail"
case statusInfo:
return "badge-info"
}
return "badge-skip"
},
"chkClass": func(s reportStatus) string {
switch s {
case statusOK:
return "chk-ok"
case statusWarn:
return "chk-warn"
case statusFail:
return "chk-fail"
case statusInfo:
return "chk-info"
}
return "chk-skip"
},
"chkIcon": func(s reportStatus) string {
switch s {
case statusOK:
return "✓"
case statusWarn:
return "!"
case statusFail:
return "✗"
case statusInfo:
return "i"
}
return "·"
},
}
var autoconfigHTMLTemplate = template.Must(template.New("autoconfig").Funcs(templateFuncs).Parse(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Autoconfiguration Report: {{.Domain}}</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
:root {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.5;
color: #1f2937;
background: #f3f4f6;
}
body { margin: 0; padding: 1rem; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
pre { background: #f3f4f6; padding: .6rem .85rem; border-radius: 6px; overflow-x: auto; font-size: .82rem; }
h1 { margin: 0 0 .4rem; font-size: 1.25rem; }
h2 { margin: 0 0 .6rem; font-size: 1rem; }
h3 { margin: 0 0 .4rem; font-size: .9rem; }
.hd, .section { background: #fff; border-radius: 10px; padding: 1rem 1.2rem; margin-bottom: .75rem; box-shadow: 0 1px 3px rgba(0,0,0,.07); }
.badge { display: inline-flex; align-items: center; padding: .2em .7em; border-radius: 9999px; font-size: .78rem; font-weight: 700; letter-spacing: .02em; }
.badge-ok { background: #d1fae5; color: #065f46; }
.badge-warn { background: #fef3c7; color: #92400e; }
.badge-fail { background: #fee2e2; color: #991b1b; }
.badge-info { background: #dbeafe; color: #1e40af; }
.badge-skip { background: #e5e7eb; color: #4b5563; }
.summary-grid { display: grid; grid-template-columns: minmax(200px, auto) 1fr; row-gap: .4rem; column-gap: 1rem; align-items: center; }
.summary-grid > .s-label { font-weight: 600; font-size: .9rem; }
.summary-grid > .s-text { font-size: .87rem; color: #4b5563; }
details { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: .45rem; overflow: hidden; }
summary { display: flex; gap: .5rem; align-items: center; padding: .55rem .85rem; cursor: pointer; user-select: none; }
summary::-webkit-details-marker { display: none; }
summary::before { content: "▶"; font-size: .65rem; color: #9ca3af; transition: transform .15s; }
details[open] > summary::before { transform: rotate(90deg); }
.probe-source { font-weight: 600; flex: 1; }
.probe-url { color: #6b7280; font-size: .78rem; word-break: break-all; }
.details-body { padding: .5rem 1rem .8rem; border-top: 1px solid #f3f4f6; font-size: .85rem; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: .3rem 1rem; margin-top: .25rem; font-size: .82rem; }
.kv > dt { color: #6b7280; font-weight: 600; }
.kv > dd { margin: 0; word-break: break-all; }
table { border-collapse: collapse; width: 100%; font-size: .85rem; }
th, td { text-align: left; padding: .35rem .55rem; border-bottom: 1px solid #f3f4f6; }
th { color: #6b7280; font-weight: 600; }
.row-strike { color: #9ca3af; text-decoration: line-through; }
.tip { border-left: 4px solid #3b82f6; padding: .75rem 1rem; background: #eff6ff; border-radius: 0 6px 6px 0; margin-bottom: .6rem; }
.tip-title { font-weight: 700; margin-bottom: .3rem; }
.tip p, .tip ul, .tip ol, .tip pre { margin: .3rem 0; }
.chk { margin-right: .3rem; }
.chk-ok { color: #059669; }
.chk-warn { color: #b45309; }
.chk-fail { color: #dc2626; }
.chk-skip { color: #6b7280; }
.chk-info { color: #1e40af; }
.mini { color: #6b7280; font-size: .82rem; }
</style>
</head>
<body>
<div class="hd">
<h1>Email Autoconfiguration: <code>{{.Domain}}</code></h1>
<span class="badge {{badgeClass .HeadlineClass}}">{{.HeadlineBadge}}</span>
<p class="mini">{{.HeadlineText}}</p>
{{if .MX}}<p class="mini">MX: {{range $i, $m := .MX}}{{if $i}}, {{end}}<code>{{$m}}</code>{{end}}</p>{{end}}
</div>
<div class="section">
<h2>Summary</h2>
<div class="summary-grid">
{{range .Summary}}
<div class="s-label">
<span class="chk {{chkClass .Status}}">{{chkIcon .Status}}</span>{{.Label}}
</div>
<div class="s-text">{{.Message}}</div>
{{end}}
</div>
</div>
{{if .Remediations}}
<div class="section">
<h2>Fix this first</h2>
{{range .Remediations}}
<div class="tip">
<div class="tip-title">{{.Title}}</div>
{{.Body}}
</div>
{{end}}
</div>
{{end}}
<div class="section">
<h2>Thunderbird-style probes ({{len .Autoconfig}})</h2>
{{range .Autoconfig}}
<details{{if eq (string .Verdict) "fail"}} open{{end}}>
<summary>
<span class="probe-source">{{.Source}}</span>
<span class="probe-url"><code>{{.URL}}</code></span>
<span class="badge {{badgeClass .Verdict}}">{{.VerdictText}}</span>
</summary>
<div class="details-body">
<dl class="kv">
{{if .StatusCode}}<dt>HTTP</dt><dd>{{.StatusCode}}</dd>{{end}}
{{if .ContentType}}<dt>Content-Type</dt><dd>{{.ContentType}}</dd>{{end}}
{{if .DurationMs}}<dt>Duration</dt><dd>{{.DurationMs}} ms</dd>{{end}}
{{if .BodyBytes}}<dt>Body size</dt><dd>{{.BodyBytes}} bytes</dd>{{end}}
{{if .Redirected}}<dt>Final URL</dt><dd><code>{{.FinalURL}}</code></dd>{{end}}
{{if .TLSSubject}}<dt>TLS subject</dt><dd>{{.TLSSubject}}</dd>{{end}}
{{if .TLSIssuer}}<dt>TLS issuer</dt><dd>{{.TLSIssuer}}</dd>{{end}}
{{if .TLSNotAfter}}<dt>Expires</dt><dd>{{.TLSNotAfter}}</dd>{{end}}
{{if .TLSError}}<dt>TLS error</dt><dd style="color:#dc2626">{{.TLSError}}</dd>{{end}}
{{if .Error}}<dt>Error</dt><dd style="color:#dc2626">{{.Error}}</dd>{{end}}
{{if .ParseError}}<dt>Parse error</dt><dd style="color:#dc2626">{{.ParseError}}</dd>{{end}}
</dl>
</div>
</details>
{{end}}
</div>
{{if .ConfigServers.Incoming}}
<div class="section">
<h2>Servers advertised by clientConfig</h2>
<h3>Incoming</h3>
<table>
<tr><th>Type</th><th>Hostname</th><th>Port</th><th>Socket</th><th>Auth</th></tr>
{{range .ConfigServers.Incoming}}
<tr>
<td>{{.Type}}</td>
<td><code>{{.Hostname}}</code></td>
<td>{{.Port}}</td>
<td>{{if .Encrypted}}<span class="chk-ok">{{.SocketType}}</span>{{else}}<span class="chk-fail">{{.SocketType}}</span>{{end}}</td>
<td>{{if .AuthSafe}}{{.Authentication}}{{else}}<span class="chk-fail">{{.Authentication}}</span>{{end}}</td>
</tr>
{{end}}
</table>
{{if .ConfigServers.Outgoing}}
<h3 style="margin-top:.7rem">Outgoing</h3>
<table>
<tr><th>Type</th><th>Hostname</th><th>Port</th><th>Socket</th><th>Auth</th></tr>
{{range .ConfigServers.Outgoing}}
<tr>
<td>{{.Type}}</td>
<td><code>{{.Hostname}}</code></td>
<td>{{.Port}}</td>
<td>{{if .Encrypted}}<span class="chk-ok">{{.SocketType}}</span>{{else}}<span class="chk-fail">{{.SocketType}}</span>{{end}}</td>
<td>{{if .AuthSafe}}{{.Authentication}}{{else}}<span class="chk-fail">{{.Authentication}}</span>{{end}}</td>
</tr>
{{end}}
</table>
{{end}}
{{if or .ConfigServers.CardDAV .ConfigServers.CalDAV}}
<h3 style="margin-top:.7rem">Personal data (xDAV)</h3>
<ul>
{{range .ConfigServers.CardDAV}}<li>CardDAV: <code>{{.ServerURL}}</code></li>{{end}}
{{range .ConfigServers.CalDAV}}<li>CalDAV: <code>{{.ServerURL}}</code></li>{{end}}
</ul>
{{end}}
</div>
{{end}}
<div class="section">
<h2>RFC 6186 SRV records</h2>
{{if .SRVRecords}}
<table>
<tr><th>Service</th><th>Target</th><th>Port</th><th>Prio</th><th>Weight</th></tr>
{{range .SRVRecords}}
<tr{{if .Skip}} class="row-strike"{{end}}>
<td><code>{{.Service}}</code></td>
<td>{{if .Skip}}<em>disabled (.)</em>{{else}}<code>{{.Target}}</code>{{end}}</td>
<td>{{.Port}}</td>
<td>{{.Priority}}</td>
<td>{{.Weight}}</td>
</tr>
{{end}}
</table>
{{else}}
<p class="mini">No SRV records found.</p>
{{end}}
</div>
<div class="section">
<h2>Microsoft Autodiscover ({{len .Autodiscover}})</h2>
{{if .Autodiscover}}
{{range .Autodiscover}}
<details{{if eq (string .Verdict) "fail"}} open{{end}}>
<summary>
<span class="probe-source">{{.Source}}</span>
<span class="probe-url"><code>{{.URL}}</code></span>
<span class="badge {{badgeClass .Verdict}}">{{.VerdictText}}</span>
</summary>
<div class="details-body">
<dl class="kv">
{{if .StatusCode}}<dt>HTTP</dt><dd>{{.StatusCode}}</dd>{{end}}
{{if .DurationMs}}<dt>Duration</dt><dd>{{.DurationMs}} ms</dd>{{end}}
{{if .TLSSubject}}<dt>TLS subject</dt><dd>{{.TLSSubject}}</dd>{{end}}
{{if .Error}}<dt>Error</dt><dd style="color:#dc2626">{{.Error}}</dd>{{end}}
{{if .ParseError}}<dt>Parse error</dt><dd style="color:#dc2626">{{.ParseError}}</dd>{{end}}
</dl>
</div>
</details>
{{end}}
{{else}}
<p class="mini">Autodiscover probes were disabled.</p>
{{end}}
</div>
<div class="section">
<h2>Example <code>config-v1.1.xml</code></h2>
<p class="mini">Paste-ready starting point. Adjust hostnames and ports before publishing.</p>
<pre>{{.ExampleXML}}</pre>
</div>
</body>
</html>`))