package hosted import ( "context" "fmt" "net" "sort" "strings" ) // DNSCheckReport summarizes live DNS checks for a hosted mail domain. type DNSCheckReport struct { Domain string `json:"domain"` TXTVerified bool `json:"txt_verified"` TXTRecords []string `json:"txt_records,omitempty"` TXTExpected string `json:"txt_expected,omitempty"` MXVerified bool `json:"mx_verified"` MXRecords []string `json:"mx_records"` ExpectedMX []string `json:"expected_mx"` Warnings []string `json:"warnings,omitempty"` Errors []string `json:"errors,omitempty"` } func LookupDomainMX(ctx context.Context, domain string) ([]string, error) { domain = strings.ToLower(strings.TrimSpace(domain)) if domain == "" { return nil, fmt.Errorf("domain required") } mxRecords, err := (&net.Resolver{}).LookupMX(ctx, domain) if err != nil { return nil, err } sort.Slice(mxRecords, func(i, j int) bool { return mxRecords[i].Pref < mxRecords[j].Pref }) out := make([]string, 0, len(mxRecords)) for _, mx := range mxRecords { host := strings.TrimSuffix(strings.ToLower(mx.Host), ".") if host != "" { out = append(out, host) } } return out, nil } func LookupDomainTXT(ctx context.Context, name string) ([]string, error) { name = strings.ToLower(strings.TrimSpace(name)) if name == "" { return nil, fmt.Errorf("txt name required") } records, err := (&net.Resolver{}).LookupTXT(ctx, name) if err != nil { return nil, err } out := make([]string, 0, len(records)) for _, record := range records { record = strings.TrimSpace(record) if record != "" { out = append(out, record) } } return out, nil } func MXMatchesExpected(mxHosts, expected []string) bool { if len(mxHosts) == 0 || len(expected) == 0 { return false } for _, mx := range mxHosts { mx = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(mx)), ".") for _, want := range expected { want = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(want)), ".") if want == "" { continue } if mx == want || strings.HasSuffix(mx, "."+want) { return true } } } return false } func TXTContainsToken(records []string, token string) bool { token = strings.TrimSpace(token) if token == "" { return false } for _, record := range records { if strings.TrimSpace(record) == token { return true } } return false } func (s *Service) CheckDomainDNS(ctx context.Context, domainID string, expectedMX []string) (DomainRow, DNSCheckReport, error) { row, err := s.GetDomain(ctx, domainID) if err != nil { return DomainRow{}, DNSCheckReport{}, err } report := DNSCheckReport{ Domain: row.Name, ExpectedMX: append([]string(nil), expectedMX...), TXTExpected: strings.TrimSpace(row.VerificationToken), } txtName := "_ultisuite-verify." + row.Name txtRecords, err := LookupDomainTXT(ctx, txtName) if err != nil { report.Errors = append(report.Errors, "txt lookup: "+err.Error()) } else { report.TXTRecords = txtRecords report.TXTVerified = TXTContainsToken(txtRecords, row.VerificationToken) if !report.TXTVerified && row.TXTVerifiedAt != nil { report.Warnings = append(report.Warnings, "txt record not found but domain was previously verified") report.TXTVerified = true } } mxRecords, err := LookupDomainMX(ctx, row.Name) if err != nil { report.Errors = append(report.Errors, "mx lookup: "+err.Error()) } else { report.MXRecords = mxRecords report.MXVerified = MXMatchesExpected(mxRecords, expectedMX) if !report.MXVerified && row.MXVerifiedAt != nil && len(expectedMX) == 0 { report.MXVerified = len(mxRecords) > 0 } } return row, report, nil } func (s *Service) VerifyDomainTXTRecord(ctx context.Context, domainID string) (DomainRow, DNSCheckReport, error) { row, report, err := s.CheckDomainDNS(ctx, domainID, nil) if err != nil { return DomainRow{}, DNSCheckReport{}, err } if !report.TXTVerified { return row, report, fmt.Errorf("txt verification token not found at _ultisuite-verify.%s", row.Name) } updated, err := s.MarkDomainVerified(ctx, domainID) if err != nil { return row, report, err } return updated, report, nil } func (s *Service) VerifyDomainMXRecord(ctx context.Context, domainID string, expectedMX []string) (DomainRow, DNSCheckReport, error) { row, report, err := s.CheckDomainDNS(ctx, domainID, expectedMX) if err != nil { return DomainRow{}, DNSCheckReport{}, err } if len(expectedMX) == 0 { report.Warnings = append(report.Warnings, "expected mx hosts not configured") return row, report, fmt.Errorf("expected mx hosts not configured") } if !report.MXVerified { return row, report, fmt.Errorf("mx records %v do not match expected %v", report.MXRecords, expectedMX) } updated, err := s.MarkDomainMXVerified(ctx, domainID) if err != nil { return row, report, err } return updated, report, nil }