269 lines
9.5 KiB
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>`))
|