Compare commits
No commits in common. "ddbcf63aec70b2d37fc981a7b11a2a18a3745cbe" and "52723bb044ae098a2242d5f3fe4a8ceaad94be79" have entirely different histories.
ddbcf63aec
...
52723bb044
9 changed files with 81 additions and 103 deletions
|
|
@ -11,6 +11,5 @@ RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_
|
|||
FROM scratch
|
||||
COPY --from=builder /checker-autoconfig /checker-autoconfig
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
USER 65534:65534
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/checker-autoconfig"]
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import (
|
|||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Real autoconfig/autodiscover documents are tiny; anything bigger is
|
||||
// misconfigured or hostile.
|
||||
// maxBodyBytes caps the response bodies we download. Autoconfig/autodiscover
|
||||
// documents are small; anything larger is either misconfigured or hostile.
|
||||
const maxBodyBytes = 256 * 1024
|
||||
|
||||
func (p *autoconfigProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
||||
|
|
@ -27,10 +27,6 @@ func (p *autoconfigProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
if domain == "" {
|
||||
return nil, fmt.Errorf("domain_name is required")
|
||||
}
|
||||
domain, err := validateDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localPart, _ := sdk.GetOption[string](opts, "probeEmail")
|
||||
if localPart == "" {
|
||||
|
|
@ -50,13 +46,6 @@ func (p *autoconfigProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
if !strings.HasSuffix(ispdbURL, "/") {
|
||||
ispdbURL += "/"
|
||||
}
|
||||
ispdbParsed, err := url.Parse(ispdbURL)
|
||||
if err != nil || (ispdbParsed.Scheme != "http" && ispdbParsed.Scheme != "https") || ispdbParsed.Host == "" {
|
||||
return nil, fmt.Errorf("invalid ispdbURL: must be an absolute http(s) URL")
|
||||
}
|
||||
if _, err := validateDomain(ispdbParsed.Hostname()); err != nil {
|
||||
return nil, fmt.Errorf("invalid ispdbURL host: %w", err)
|
||||
}
|
||||
userAgent, _ := sdk.GetOption[string](opts, "userAgent")
|
||||
if userAgent == "" {
|
||||
userAgent = "happyDomain-autoconfig/1.0 (+https://happydomain.org)"
|
||||
|
|
@ -110,7 +99,7 @@ func (p *autoconfigProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
})
|
||||
}
|
||||
|
||||
// Runs synchronously: needs MX, but overlaps the SRV/Autodiscover goroutines.
|
||||
// Autoconfig probes run after MX (MX parent needed), overlapping with the SRV/Autodiscover goroutines.
|
||||
data.Autoconfig = collectAutoconfig(ctx, client, userAgent, domain, email, data.MX, tryISPDB, tryHTTPAutoconfig, ispdbURL)
|
||||
for _, p := range data.Autoconfig {
|
||||
if p.Parsed != nil {
|
||||
|
|
@ -125,8 +114,8 @@ func (p *autoconfigProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
}
|
||||
|
||||
func newHTTPClient(timeout time.Duration) *http.Client {
|
||||
// Keep cert validation ON; failures are surfaced as soft probe errors
|
||||
// so the rule engine can flag them.
|
||||
// We keep certificate validation ON and surface the failure as a
|
||||
// soft error on the probe, so the rule engine can flag it.
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: 8,
|
||||
|
|
@ -170,6 +159,7 @@ func fetch(ctx context.Context, client *http.Client, userAgent, method, rawURL s
|
|||
res.DurationMs = time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
// Try to distinguish a TLS verification failure.
|
||||
var tlsErr *tls.CertificateVerificationError
|
||||
if errors.As(err, &tlsErr) {
|
||||
res.TLSError = err.Error()
|
||||
|
|
@ -185,6 +175,7 @@ func fetch(ctx context.Context, client *http.Client, userAgent, method, rawURL s
|
|||
|
||||
if resp.TLS != nil {
|
||||
res.TLSServerName = resp.TLS.ServerName
|
||||
res.TLSValid = true
|
||||
if len(resp.TLS.PeerCertificates) > 0 {
|
||||
leaf := resp.TLS.PeerCertificates[0]
|
||||
res.TLSSubject = leaf.Subject.CommonName
|
||||
|
|
@ -225,8 +216,10 @@ func collectAutoconfig(ctx context.Context, client *http.Client, userAgent, doma
|
|||
if tryISPDB {
|
||||
targets = append(targets, target{"ispdb", ispdbURL + domain})
|
||||
}
|
||||
// MX fallback catches gmail/MS365-hosted domains. Bucksch suggests
|
||||
// iterating every MX; the lowest-preference one is enough in practice.
|
||||
// MX fallback: repeat with the registrable parent domain of the MX
|
||||
// host. We pick the lowest-preference MX for simplicity (Bucksch
|
||||
// suggests iterating; one is enough to catch gmail/MS365-hosted
|
||||
// domains in practice).
|
||||
if mxParent := pickMXParent(mx); mxParent != "" && mxParent != domain {
|
||||
targets = append(targets, target{"mx-autoconfig", fmt.Sprintf("https://autoconfig.%s/mail/config-v1.1.xml?emailaddress=%s", mxParent, encoded)})
|
||||
if tryISPDB {
|
||||
|
|
@ -264,38 +257,6 @@ func runAutoconfigProbe(ctx context.Context, client *http.Client, userAgent, sou
|
|||
return probe
|
||||
}
|
||||
|
||||
// validateDomain rejects anything that could escape URL interpolation
|
||||
// (path/query injection, IP literals). IP-range filtering is left to the
|
||||
// network layer.
|
||||
func validateDomain(domain string) (string, error) {
|
||||
domain = strings.ToLower(domain)
|
||||
if len(domain) == 0 || len(domain) > 253 {
|
||||
return "", fmt.Errorf("invalid domain name: length must be 1..253")
|
||||
}
|
||||
if net.ParseIP(domain) != nil {
|
||||
return "", fmt.Errorf("invalid domain name: IP literals are not accepted")
|
||||
}
|
||||
labels := strings.Split(domain, ".")
|
||||
if len(labels) < 2 {
|
||||
return "", fmt.Errorf("invalid domain name: must contain at least one dot")
|
||||
}
|
||||
for _, label := range labels {
|
||||
if len(label) == 0 || len(label) > 63 {
|
||||
return "", fmt.Errorf("invalid domain name: label length must be 1..63")
|
||||
}
|
||||
if label[0] == '-' || label[len(label)-1] == '-' {
|
||||
return "", fmt.Errorf("invalid domain name: label %q cannot start or end with '-'", label)
|
||||
}
|
||||
for i := 0; i < len(label); i++ {
|
||||
c := label[i]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
|
||||
return "", fmt.Errorf("invalid domain name: label %q contains forbidden character", label)
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
// pickMXParent returns the parent domain of the lowest-preference MX, or
|
||||
// empty when no suitable MX is present.
|
||||
func pickMXParent(mx []MXRecord) string {
|
||||
|
|
@ -311,9 +272,12 @@ func pickMXParent(mx []MXRecord) string {
|
|||
return registrableDomain(best.Host)
|
||||
}
|
||||
|
||||
// registrableDomain approximates a PSL lookup with last-two-labels (or
|
||||
// three when the SLD looks like a ccTLD second level, e.g. co.uk). Good
|
||||
// enough for the gmail / MS365 MX-fallback case we actually care about.
|
||||
// registrableDomain returns a best-effort "effective" domain. We do not
|
||||
// ship a Public Suffix List; stripping one label covers the common case
|
||||
// (e.g. aspmx.l.google.com → l.google.com, while l.google.com →
|
||||
// google.com after a second call; the caller only needs one level).
|
||||
// For the MX case we approximate with the last two labels, falling back
|
||||
// to three when the second-to-last label is short (e.g. co.uk).
|
||||
func registrableDomain(host string) string {
|
||||
host = strings.TrimSuffix(strings.ToLower(host), ".")
|
||||
parts := strings.Split(host, ".")
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import (
|
|||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Version is overridden at link time by the build, or by the plugin loader.
|
||||
// Version is the checker version reported in CheckerDefinition.Version.
|
||||
var Version = "built-in"
|
||||
|
||||
// Definition returns the CheckerDefinition for the autoconfig checker.
|
||||
func Definition() *sdk.CheckerDefinition {
|
||||
return &sdk.CheckerDefinition{
|
||||
ID: "email-autoconfig",
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ package checker
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// RenderForm describes the standalone /check page inputs.
|
||||
// RenderForm advertises the minimal inputs a human needs to run the
|
||||
// checker from its standalone /check page.
|
||||
func (p *autoconfigProvider) RenderForm() []sdk.CheckerOptionField {
|
||||
return []sdk.CheckerOptionField{
|
||||
{
|
||||
|
|
@ -54,13 +54,10 @@ func (p *autoconfigProvider) RenderForm() []sdk.CheckerOptionField {
|
|||
}
|
||||
}
|
||||
|
||||
// ParseForm builds CheckerOptions from the submitted form. All probing is
|
||||
// deferred to Collect, so we only validate the domain shape here.
|
||||
// ParseForm turns a submitted /check form into the CheckerOptions the
|
||||
// Collect step expects. Collect itself handles all DNS/HTTP probing, so
|
||||
// there is nothing to resolve up-front here beyond accepting the domain.
|
||||
func (p *autoconfigProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("parse form: %w", err)
|
||||
}
|
||||
|
||||
domain := strings.TrimSuffix(strings.TrimSpace(r.FormValue(sdk.AutoFillDomainName)), ".")
|
||||
if domain == "" {
|
||||
return nil, errors.New("domain name is required")
|
||||
|
|
@ -72,14 +69,9 @@ func (p *autoconfigProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, err
|
|||
if v := strings.TrimSpace(r.FormValue("probeEmail")); v != "" {
|
||||
opts["probeEmail"] = v
|
||||
}
|
||||
|
||||
// HTML omits unchecked boxes; treat absence as "use the documented default".
|
||||
for _, key := range []string{"tryISPDB", "tryHTTPAutoconfig", "tryAutodiscoverPost"} {
|
||||
if _, ok := r.Form[key]; !ok {
|
||||
continue
|
||||
}
|
||||
opts[key] = r.FormValue(key) == "true"
|
||||
}
|
||||
opts["tryISPDB"] = r.FormValue("tryISPDB") == "true"
|
||||
opts["tryHTTPAutoconfig"] = r.FormValue("tryHTTPAutoconfig") == "true"
|
||||
opts["tryAutodiscoverPost"] = r.FormValue("tryAutodiscoverPost") == "true"
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func parseClientConfig(body []byte) (*ClientConfig, error) {
|
|||
if len(body) == 0 {
|
||||
return nil, fmt.Errorf("empty body")
|
||||
}
|
||||
// Cheap reject before invoking the XML decoder.
|
||||
// Fast sanity check: must contain clientConfig.
|
||||
if !bytes.Contains(body, []byte("<clientConfig")) {
|
||||
return nil, fmt.Errorf("not a clientConfig document")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Provider returns a new autoconfig observation provider.
|
||||
func Provider() sdk.ObservationProvider {
|
||||
return &autoconfigProvider{}
|
||||
}
|
||||
|
|
@ -14,6 +15,7 @@ func (p *autoconfigProvider) Key() sdk.ObservationKey {
|
|||
return ObservationKeyAutoconfig
|
||||
}
|
||||
|
||||
// Definition implements sdk.CheckerDefinitionProvider.
|
||||
func (p *autoconfigProvider) Definition() *sdk.CheckerDefinition {
|
||||
return Definition()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ func buildReport(d *Data) reportData {
|
|||
r.MX = append(r.MX, fmt.Sprintf("%s (pref %d)", m.Host, m.Preference))
|
||||
}
|
||||
|
||||
// Autoconfig probes.
|
||||
hasParsed := false
|
||||
hasAutoconfigOK := false
|
||||
hasWellKnownOK := false
|
||||
|
|
@ -138,6 +139,7 @@ func buildReport(d *Data) reportData {
|
|||
r.Autoconfig = append(r.Autoconfig, rp)
|
||||
}
|
||||
|
||||
// Autodiscover probes.
|
||||
hasAutodiscover := false
|
||||
for _, p := range d.Autodiscover {
|
||||
rp := reportProbe{ProbeResult: p.Result}
|
||||
|
|
@ -163,6 +165,7 @@ func buildReport(d *Data) reportData {
|
|||
r.Autodiscover = append(r.Autodiscover, rp)
|
||||
}
|
||||
|
||||
// SRV.
|
||||
hasIncomingSRV, hasSubmissionSRV := false, false
|
||||
for _, s := range d.SRV {
|
||||
if !s.Skip {
|
||||
|
|
@ -173,6 +176,7 @@ func buildReport(d *Data) reportData {
|
|||
}
|
||||
r.SRVRecords = d.SRV
|
||||
|
||||
// Servers advertised in clientConfig.
|
||||
if d.ClientConfig != nil {
|
||||
for _, s := range d.ClientConfig.Incoming {
|
||||
r.ConfigServers.Incoming = append(r.ConfigServers.Incoming, toReportServer(s))
|
||||
|
|
@ -184,6 +188,7 @@ func buildReport(d *Data) reportData {
|
|||
r.ConfigServers.CalDAV = d.ClientConfig.Calendar
|
||||
}
|
||||
|
||||
// Headline + summary.
|
||||
switch {
|
||||
case hasParsed && hasAutoconfigOK && tlsFailures == 0 && !plaintextOK:
|
||||
r.HeadlineBadge, r.HeadlineClass, r.HeadlineText = "OK", statusOK, "Email autoconfiguration is published and healthy."
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package checker
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// getData is the shared helper every rule starts with.
|
||||
func getData(ctx context.Context, obs sdk.ObservationGetter) (*Data, *sdk.CheckState) {
|
||||
var d Data
|
||||
if err := obs.Get(ctx, ObservationKeyAutoconfig, &d); err != nil {
|
||||
|
|
@ -40,6 +40,7 @@ func (r *presenceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter,
|
|||
return single(*errState)
|
||||
}
|
||||
|
||||
// Successful Thunderbird-style parse counts.
|
||||
for _, p := range d.Autoconfig {
|
||||
if p.Parsed != nil {
|
||||
return single(sdk.CheckState{
|
||||
|
|
@ -108,7 +109,7 @@ func (r *preferredEndpointRule) Evaluate(ctx context.Context, obs sdk.Observatio
|
|||
// When nothing works, let the presence rule drive the verdict.
|
||||
if !anyOK {
|
||||
return single(sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Status: sdk.StatusInfo,
|
||||
Message: "No autoconfig responded; primary endpoint not evaluated.",
|
||||
Code: "autoconfig_preferred_skip",
|
||||
})
|
||||
|
|
@ -192,7 +193,7 @@ func (r *tlsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts
|
|||
|
||||
if len(out) == 0 {
|
||||
return single(sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Status: sdk.StatusInfo,
|
||||
Message: "No autoconfig probe reached an endpoint; TLS not assessed.",
|
||||
Code: "autoconfig_tls_skip",
|
||||
})
|
||||
|
|
@ -218,7 +219,7 @@ func (r *encryptionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter
|
|||
}
|
||||
if d.ClientConfig == nil {
|
||||
return single(sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Status: sdk.StatusInfo,
|
||||
Message: "No clientConfig parsed; encryption check skipped.",
|
||||
Code: "autoconfig_encryption_skip",
|
||||
})
|
||||
|
|
@ -239,7 +240,7 @@ func (r *encryptionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter
|
|||
Code: "autoconfig_plaintext_server",
|
||||
})
|
||||
case strings.EqualFold(s.Authentication, "password-cleartext"):
|
||||
// Cleartext password is fine here because the transport is encrypted.
|
||||
// isEncryptedSocket is true here, so the cleartext password is tunneled (informational).
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusOK,
|
||||
Subject: subject,
|
||||
|
|
@ -257,7 +258,7 @@ func (r *encryptionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter
|
|||
}
|
||||
if len(out) == 0 {
|
||||
return single(sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Status: sdk.StatusInfo,
|
||||
Message: "clientConfig declares no server to evaluate.",
|
||||
Code: "autoconfig_encryption_skip",
|
||||
})
|
||||
|
|
@ -283,7 +284,7 @@ func (r *consistencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGette
|
|||
}
|
||||
if d.ClientConfig == nil {
|
||||
return single(sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Status: sdk.StatusInfo,
|
||||
Message: "No clientConfig to compare.",
|
||||
Code: "autoconfig_consistency_skip",
|
||||
})
|
||||
|
|
@ -467,15 +468,8 @@ func srvVersusServers(srv []SRVRecord, incoming, outgoing []ServerConfig) []srvM
|
|||
byType[r.Service] = append(byType[r.Service], r)
|
||||
}
|
||||
|
||||
services := make([]string, 0, len(byType))
|
||||
for svc := range byType {
|
||||
services = append(services, svc)
|
||||
}
|
||||
sort.Strings(services)
|
||||
|
||||
var out []srvMismatch
|
||||
for _, svc := range services {
|
||||
recs := byType[svc]
|
||||
for svc, recs := range byType {
|
||||
var configs []ServerConfig
|
||||
switch svc {
|
||||
case "_imaps._tcp", "_imap._tcp":
|
||||
|
|
@ -500,7 +494,7 @@ func srvVersusServers(srv []SRVRecord, incoming, outgoing []ServerConfig) []srvM
|
|||
if !match {
|
||||
out = append(out, srvMismatch{
|
||||
service: svc,
|
||||
message: fmt.Sprintf("No SRV %s record matches any clientConfig %s server (host/port)", svc, configs[0].Type),
|
||||
message: fmt.Sprintf("SRV %s points outside clientConfig's %s servers", svc, configs[0].Type),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
// Package checker probes a domain for the three competing email
|
||||
// autoconfiguration mechanisms (Bucksch autoconfig, Microsoft Autodiscover
|
||||
// POX, RFC 6186 SRV) and cross-checks them.
|
||||
// Package checker implements the email autoconfiguration checker for
|
||||
// happyDomain.
|
||||
//
|
||||
// It verifies that a domain publishes discoverable email client
|
||||
// configuration, as described in Bucksch' autoconfig draft
|
||||
// (draft-bucksch-autoconfig-00), Microsoft's Autodiscover (POX) and RFC
|
||||
// 6186 SRV records. The checker probes every standard endpoint, parses
|
||||
// the returned documents, cross-checks the servers they advertise and
|
||||
// rates the result with user-actionable hints.
|
||||
package checker
|
||||
|
||||
import (
|
||||
|
|
@ -16,18 +22,27 @@ type Data struct {
|
|||
Email string `json:"email"`
|
||||
CollectedAt time.Time `json:"collected_at"`
|
||||
|
||||
// MX records found on the domain.
|
||||
MX []MXRecord `json:"mx,omitempty"`
|
||||
MXError string `json:"mx_error,omitempty"`
|
||||
|
||||
// RFC 6186 style SRV records.
|
||||
SRV []SRVRecord `json:"srv,omitempty"`
|
||||
|
||||
Autoconfig []AutoconfigProbe `json:"autoconfig,omitempty"`
|
||||
// Thunderbird autoconfig probes.
|
||||
Autoconfig []AutoconfigProbe `json:"autoconfig,omitempty"`
|
||||
|
||||
// Microsoft Autodiscover probes.
|
||||
Autodiscover []AutodiscoverProbe `json:"autodiscover,omitempty"`
|
||||
|
||||
// First successful autoconfig parse, promoted here for rules to consume.
|
||||
ClientConfig *ClientConfig `json:"client_config,omitempty"`
|
||||
ClientConfigSource string `json:"client_config_source,omitempty"`
|
||||
// The "best" (first successful) autoconfig document we received,
|
||||
// fully parsed. Present only when at least one probe succeeded.
|
||||
ClientConfig *ClientConfig `json:"client_config,omitempty"`
|
||||
|
||||
// Source of the ClientConfig above ("autoconfig", "ispdb", "mx").
|
||||
ClientConfigSource string `json:"client_config_source,omitempty"`
|
||||
|
||||
// The Autodiscover response we parsed, if any.
|
||||
AutodiscoverResult *AutodiscoverResponse `json:"autodiscover_result,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -39,12 +54,14 @@ type MXRecord struct {
|
|||
|
||||
// SRVRecord is a single RFC 6186 SRV record observation.
|
||||
type SRVRecord struct {
|
||||
Service string `json:"service"` // RFC 6186 tag, e.g. "_imaps._tcp"
|
||||
// Service is the RFC 6186 service tag, e.g. "_imaps._tcp".
|
||||
Service string `json:"service"`
|
||||
Target string `json:"target"`
|
||||
Port uint16 `json:"port"`
|
||||
Priority uint16 `json:"priority"`
|
||||
Weight uint16 `json:"weight"`
|
||||
// Skip means the service is explicitly disabled (RFC 6186 target ".").
|
||||
// Skip is true when RFC 6186 indicates the service is explicitly
|
||||
// unsupported (target ".").
|
||||
Skip bool `json:"skip,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +79,7 @@ type ProbeResult struct {
|
|||
TLSIssuer string `json:"tls_issuer,omitempty"`
|
||||
TLSSubject string `json:"tls_subject,omitempty"`
|
||||
TLSNotAfter string `json:"tls_not_after,omitempty"`
|
||||
TLSValid bool `json:"tls_valid,omitempty"`
|
||||
TLSError string `json:"tls_error,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ParseError string `json:"parse_error,omitempty"`
|
||||
|
|
@ -127,15 +145,18 @@ type Documentation struct {
|
|||
|
||||
// AutodiscoverResponse is the parsed POX response.
|
||||
type AutodiscoverResponse struct {
|
||||
// User.DisplayName
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
// Set when Action is redirectAddr / redirectUrl; mutually exclusive with Protocols.
|
||||
RedirectAddr string `json:"redirect_addr,omitempty"`
|
||||
RedirectURL string `json:"redirect_url,omitempty"`
|
||||
Protocols []AutodiscoverProtocol `json:"protocols,omitempty"`
|
||||
// Redirect destinations (when Action=redirectAddr/redirectUrl).
|
||||
RedirectAddr string `json:"redirect_addr,omitempty"`
|
||||
RedirectURL string `json:"redirect_url,omitempty"`
|
||||
// Protocols configured by the server.
|
||||
Protocols []AutodiscoverProtocol `json:"protocols,omitempty"`
|
||||
}
|
||||
|
||||
// AutodiscoverProtocol covers IMAP/POP/SMTP fields; Exchange-only protocols
|
||||
// (EXCH/EXPR/MobileSync) are stored but not analysed.
|
||||
// AutodiscoverProtocol mirrors the subset of <Protocol> elements relevant
|
||||
// to IMAP/POP/SMTP configuration. Exchange-specific protocols are captured
|
||||
// but not deeply analysed.
|
||||
type AutodiscoverProtocol struct {
|
||||
Type string `json:"type"` // IMAP, POP3, SMTP, EXCH, EXPR, WEB, MobileSync
|
||||
Server string `json:"server,omitempty"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue