Initial commit
This commit is contained in:
commit
c4bf833274
19 changed files with 2451 additions and 0 deletions
202
checker/parse.go
Normal file
202
checker/parse.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue