package app import ( "context" "fmt" "net" "net/http" "strings" "time" "github.com/go-chi/chi/v5" ) func (a *App) handleDNSRecords(w http.ResponseWriter, r *http.Request) { domain, err := a.domainByID(r.Context(), chi.URLParam(r, "id")) if err != nil { respondError(w, http.StatusNotFound, "domain not found") return } respondJSON(w, http.StatusOK, map[string]any{"items": a.dnsRecordsFor(domain)}) } func (a *App) handleDNSCheck(w http.ResponseWriter, r *http.Request) { domain, err := a.domainByID(r.Context(), chi.URLParam(r, "id")) if err != nil { respondError(w, http.StatusNotFound, "domain not found") return } result := a.checkDNS(r.Context(), domain) now := a.now().UTC().Format(time.RFC3339Nano) _, _ = a.db.ExecContext(r.Context(), `UPDATE domains SET dns_status=?, dns_checked_at=?, updated_at=? WHERE id=?`, result.Status, now, now, domain.ID) respondJSON(w, http.StatusOK, result) } func (a *App) dnsRecordsFor(d *Domain) []DNSRecord { name := strings.TrimSuffix(d.Name, ".") host := strings.TrimSuffix(a.cfg.PublicHostname, ".") + "." return []DNSRecord{ {Type: "MX", Name: name, Value: fmt.Sprintf("10 %s", host), TTL: 300}, {Type: "TXT", Name: name, Value: "v=spf1 mx -all", TTL: 300}, {Type: "TXT", Name: d.DKIMSelector + "._domainkey." + name, Value: "v=DKIM1; k=rsa; p=" + d.DKIMPublicKey, TTL: 300}, {Type: "TXT", Name: "_dmarc." + name, Value: "v=DMARC1; p=quarantine; rua=mailto:postmaster@" + name, TTL: 300}, } } func (a *App) checkDNS(ctx context.Context, d *Domain) DNSCheckResult { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() resolver := net.DefaultResolver checks := map[string]DNSCheckStatus{} mx, err := resolver.LookupMX(ctx, d.Name) if err != nil || len(mx) == 0 { checks["mx"] = DNSCheckStatus{OK: false, Message: "未找到 MX 记录"} } else { found := make([]string, 0, len(mx)) ok := false for _, item := range mx { entry := fmt.Sprintf("%d %s", item.Pref, strings.TrimSuffix(item.Host, ".")) found = append(found, entry) if strings.EqualFold(strings.TrimSuffix(item.Host, "."), strings.TrimSuffix(a.cfg.PublicHostname, ".")) { ok = true } } checks["mx"] = DNSCheckStatus{OK: ok, Message: boolMessage(ok, "MX 指向正确", "MX 未指向当前邮件主机"), Found: found} } rootTXT, _ := resolver.LookupTXT(ctx, d.Name) checks["spf"] = txtContains(rootTXT, "v=spf1", "SPF 记录存在", "未找到 SPF 记录") dkimName := d.DKIMSelector + "._domainkey." + d.Name dkimTXT, _ := resolver.LookupTXT(ctx, dkimName) checks["dkim"] = txtContains(dkimTXT, "v=DKIM1", "DKIM 记录存在", "未找到 DKIM 记录") dmarcTXT, _ := resolver.LookupTXT(ctx, "_dmarc."+d.Name) checks["dmarc"] = txtContains(dmarcTXT, "v=DMARC1", "DMARC 记录存在", "未找到 DMARC 记录") status := "ok" for _, c := range checks { if !c.OK { status = "error" break } } return DNSCheckResult{Domain: d.Name, Status: status, Checks: checks} } func txtContains(records []string, needle, okMsg, failMsg string) DNSCheckStatus { found := append([]string{}, records...) for _, item := range records { if strings.Contains(strings.ToLower(item), strings.ToLower(needle)) { return DNSCheckStatus{OK: true, Message: okMsg, Found: found} } } return DNSCheckStatus{OK: false, Message: failMsg, Found: found} } func boolMessage(ok bool, yes, no string) string { if ok { return yes } return no }