202 lines
6.3 KiB
Go
202 lines
6.3 KiB
Go
package checker
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ── Thunderbird autoconfig (clientConfig) ────────────────────────────────────
|
|
|
|
type rawClientConfig struct {
|
|
XMLName xml.Name `xml:"clientConfig"`
|
|
Version string `xml:"version,attr"`
|
|
EmailProvider rawEmailProvider `xml:"emailProvider"`
|
|
}
|
|
|
|
type rawEmailProvider struct {
|
|
ID string `xml:"id,attr"`
|
|
DisplayName string `xml:"displayName"`
|
|
ShortName string `xml:"displayShortName"`
|
|
Domains []string `xml:"domain"`
|
|
Incoming []rawServer `xml:"incomingServer"`
|
|
Outgoing []rawServer `xml:"outgoingServer"`
|
|
AddressBook []rawDav `xml:"addressBook"`
|
|
Calendar []rawDav `xml:"calendar"`
|
|
WebMail *rawWebMail `xml:"webMail"`
|
|
Documentation []rawDocuments `xml:"documentation"`
|
|
}
|
|
|
|
type rawServer struct {
|
|
Type string `xml:"type,attr"`
|
|
Hostname string `xml:"hostname"`
|
|
Port string `xml:"port"`
|
|
SocketType string `xml:"socketType"`
|
|
Username string `xml:"username"`
|
|
Authentication string `xml:"authentication"`
|
|
}
|
|
|
|
type rawDav struct {
|
|
Type string `xml:"type,attr"`
|
|
Username string `xml:"username"`
|
|
Authentication string `xml:"authentication"`
|
|
ServerURL string `xml:"serverURL"`
|
|
}
|
|
|
|
type rawWebMail struct {
|
|
LoginPage struct {
|
|
URL string `xml:"url,attr"`
|
|
} `xml:"loginPage"`
|
|
}
|
|
|
|
type rawDocuments struct {
|
|
URL string `xml:"url,attr"`
|
|
Descr string `xml:"descr"`
|
|
}
|
|
|
|
// parseClientConfig decodes a clientConfig document.
|
|
func parseClientConfig(body []byte) (*ClientConfig, error) {
|
|
body = bytes.TrimSpace(body)
|
|
if len(body) == 0 {
|
|
return nil, fmt.Errorf("empty body")
|
|
}
|
|
// Cheap reject before invoking the XML decoder.
|
|
if !bytes.Contains(body, []byte("<clientConfig")) {
|
|
return nil, fmt.Errorf("not a clientConfig document")
|
|
}
|
|
|
|
var raw rawClientConfig
|
|
if err := xml.Unmarshal(body, &raw); err != nil {
|
|
return nil, fmt.Errorf("XML decode: %w", err)
|
|
}
|
|
if len(raw.EmailProvider.Incoming) == 0 && len(raw.EmailProvider.Outgoing) == 0 {
|
|
return nil, fmt.Errorf("no incoming/outgoing server defined")
|
|
}
|
|
|
|
cfg := &ClientConfig{
|
|
Version: raw.Version,
|
|
EmailProviderID: raw.EmailProvider.ID,
|
|
DisplayName: raw.EmailProvider.DisplayName,
|
|
ShortName: raw.EmailProvider.ShortName,
|
|
Domains: raw.EmailProvider.Domains,
|
|
}
|
|
|
|
for _, s := range raw.EmailProvider.Incoming {
|
|
cfg.Incoming = append(cfg.Incoming, convertServer(s))
|
|
}
|
|
for _, s := range raw.EmailProvider.Outgoing {
|
|
cfg.Outgoing = append(cfg.Outgoing, convertServer(s))
|
|
}
|
|
for _, d := range raw.EmailProvider.AddressBook {
|
|
cfg.AddressBook = append(cfg.AddressBook, convertDav(d))
|
|
}
|
|
for _, d := range raw.EmailProvider.Calendar {
|
|
cfg.Calendar = append(cfg.Calendar, convertDav(d))
|
|
}
|
|
if raw.EmailProvider.WebMail != nil && raw.EmailProvider.WebMail.LoginPage.URL != "" {
|
|
cfg.WebMail = &WebMail{LoginPage: raw.EmailProvider.WebMail.LoginPage.URL}
|
|
}
|
|
for _, d := range raw.EmailProvider.Documentation {
|
|
cfg.Documentation = append(cfg.Documentation, Documentation{URL: d.URL, Descr: d.Descr})
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func convertDav(d rawDav) DavServer {
|
|
return DavServer{
|
|
Type: d.Type,
|
|
Username: d.Username,
|
|
Authentication: d.Authentication,
|
|
ServerURL: d.ServerURL,
|
|
}
|
|
}
|
|
|
|
func convertServer(s rawServer) ServerConfig {
|
|
port, _ := strconv.Atoi(strings.TrimSpace(s.Port))
|
|
return ServerConfig{
|
|
Type: strings.ToLower(strings.TrimSpace(s.Type)),
|
|
Hostname: strings.TrimSpace(s.Hostname),
|
|
Port: port,
|
|
SocketType: strings.TrimSpace(s.SocketType),
|
|
Username: strings.TrimSpace(s.Username),
|
|
Authentication: strings.TrimSpace(s.Authentication),
|
|
}
|
|
}
|
|
|
|
// ── Microsoft Autodiscover POX response ──────────────────────────────────────
|
|
|
|
type rawAutodiscover struct {
|
|
XMLName xml.Name `xml:"Autodiscover"`
|
|
Response rawADResponse `xml:"Response"`
|
|
}
|
|
|
|
type rawADResponse struct {
|
|
User rawADUser `xml:"User"`
|
|
Account rawADAccount `xml:"Account"`
|
|
}
|
|
|
|
type rawADUser struct {
|
|
DisplayName string `xml:"DisplayName"`
|
|
}
|
|
|
|
type rawADAccount struct {
|
|
Action string `xml:"Action"`
|
|
RedirectAddr string `xml:"RedirectAddr"`
|
|
RedirectURL string `xml:"RedirectUrl"`
|
|
Protocols []rawADProto `xml:"Protocol"`
|
|
}
|
|
|
|
type rawADProto struct {
|
|
Type string `xml:"Type"`
|
|
Server string `xml:"Server"`
|
|
Port string `xml:"Port"`
|
|
Encryption string `xml:"Encryption"`
|
|
SSL string `xml:"SSL"`
|
|
LoginName string `xml:"LoginName"`
|
|
DomainRequired string `xml:"DomainRequired"`
|
|
AuthRequired string `xml:"AuthRequired"`
|
|
}
|
|
|
|
func parseAutodiscoverResponse(body []byte) (*AutodiscoverResponse, error) {
|
|
body = bytes.TrimSpace(body)
|
|
if len(body) == 0 {
|
|
return nil, fmt.Errorf("empty body")
|
|
}
|
|
if !bytes.Contains(body, []byte("Autodiscover")) {
|
|
return nil, fmt.Errorf("not an Autodiscover document")
|
|
}
|
|
|
|
var raw rawAutodiscover
|
|
dec := xml.NewDecoder(bytes.NewReader(body))
|
|
// Exchange responses are namespaced; we accept any.
|
|
dec.Strict = false
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("XML decode: %w", err)
|
|
}
|
|
|
|
r := &AutodiscoverResponse{
|
|
DisplayName: strings.TrimSpace(raw.Response.User.DisplayName),
|
|
RedirectAddr: strings.TrimSpace(raw.Response.Account.RedirectAddr),
|
|
RedirectURL: strings.TrimSpace(raw.Response.Account.RedirectURL),
|
|
}
|
|
for _, p := range raw.Response.Account.Protocols {
|
|
port, _ := strconv.Atoi(strings.TrimSpace(p.Port))
|
|
r.Protocols = append(r.Protocols, AutodiscoverProtocol{
|
|
Type: strings.ToUpper(strings.TrimSpace(p.Type)),
|
|
Server: strings.TrimSpace(p.Server),
|
|
Port: port,
|
|
Encryption: strings.TrimSpace(p.Encryption),
|
|
SSL: strings.TrimSpace(p.SSL),
|
|
LoginName: strings.TrimSpace(p.LoginName),
|
|
DomainRequired: strings.TrimSpace(p.DomainRequired),
|
|
AuthRequired: strings.TrimSpace(p.AuthRequired),
|
|
})
|
|
}
|
|
// A bare redirect is valid; accept it.
|
|
if len(r.Protocols) == 0 && r.RedirectAddr == "" && r.RedirectURL == "" {
|
|
return nil, fmt.Errorf("Autodiscover response has no protocol or redirect")
|
|
}
|
|
return r, nil
|
|
}
|