package handlers import ( "errors" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "testing" "github.com/mixeme/selfpost/internal/dmarc" "github.com/mixeme/selfpost/internal/domain" "github.com/mixeme/selfpost/internal/health" "github.com/mixeme/selfpost/internal/postfix" "github.com/mixeme/selfpost/internal/store" "github.com/mixeme/selfpost/internal/web/auth" ) // postFormAs sends a form to a handler as the given principal. func postFormAs(h http.HandlerFunc, p auth.Principal, target string, pathValues map[string]string, values url.Values) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(values.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") for k, v := range pathValues { req.SetPathValue(k, v) } rec := httptest.NewRecorder() h(rec, auth.RequestWithPrincipal(req, p)) return rec } // dmarcRecordingMaps stands in for Postfix: the allow-lists a resync would write. type dmarcRecordingMaps struct{ got []postfix.DMARCMapsConfig } func (m *dmarcRecordingMaps) RebuildDMARCMaps(cfg postfix.DMARCMapsConfig) error { m.got = append(m.got, cfg) return nil } // hostedStand is an app stand whose server receives hosted DMARC reports. func hostedStand(t *testing.T) (appStand, *dmarcRecordingMaps) { t.Helper() s := newAppStand(t) maps := &dmarcRecordingMaps{} s.h.cfg.DMARCEnabled, s.h.cfg.Hostname = true, "mail.example.org" s.h.dmarc = dmarc.NewService(s.h.store, maps, "mail.example.org", true) return s, maps } const hostedForExample = "dmarc-reports+example.org@mail.example.org" // hostedEscaped is that address as html/template writes it into a page. const hostedEscaped = "dmarc-reports+example.org@mail.example.org" // saveReports posts the report-address form of domain 1 as the global // administrator. func (s appStand) saveReports(form url.Values) *httptest.ResponseRecorder { return postFormAs(s.h.HandleDomainDMARC, globalPrincipal, "/outbound/domains/1/settings/reports", s.paths, form) } func (s appStand) reportAddress(t *testing.T) string { t.Helper() d, err := s.h.store.GetDomain(s.d.ID) if err != nil { t.Fatal(err) } return d.DMARCRua } func contains(list []string, want string) bool { for _, s := range list { if s == want { return true } } return false } // A new domain has no report address: its settings show "No reports" selected, // and its DMARC record says so. func TestNewDomainHasNoReports(t *testing.T) { s, _ := hostedStand(t) keys := domain.NewKeys(t.TempDir()) if _, err := keys.EnsureKey("example.org", "mail"); err != nil { t.Fatal(err) } s.h.domains = domain.NewService(s.h.store, keys, s.h.apps, "mail") rec := postFormAs(s.h.HandleAddDomain, globalPrincipal, "/outbound/domains", nil, url.Values{"name": {"new.example"}}) if rec.Code != http.StatusSeeOther { t.Fatalf("adding a domain = %d:\n%s", rec.Code, rec.Body.String()) } domains, _ := s.h.store.ListDomains() var added store.Domain for _, d := range domains { if d.Name == "new.example" { added = d } } if added.ID == 0 || added.DMARCRua != "" { t.Fatalf("a new domain reports to %q (%+v), want no reports", added.DMARCRua, added) } body := send(s.h.HandleDomainSettings, &globalPrincipal, "GET", "/outbound/domains/2/settings", map[string]string{"id": idStr(added.ID)}, nil).Body.String() has(t, "settings of a new domain", body, ``) lacks(t, "settings of a new domain", body, ``, ``, ``) if rec := s.saveReports(url.Values{"dmarc_rua_mode": {"hosted"}}); rec.Code != http.StatusSeeOther { t.Fatalf("hosted = %d:\n%s", rec.Code, rec.Body.String()) } if got := s.reportAddress(t); got != hostedForExample { t.Fatalf("hosted stored %q, want %q", got, hostedForExample) } has(t, "settings after hosted", settings(), ``, ``) if err := s.h.store.SetDomainDMARCAddress(s.d.ID, "reports@hub.example"); err != nil { t.Fatal(err) } for _, form := range []url.Values{ {"dmarc_rua_mode": {"hosted"}}, {"dmarc_rua_mode": {"inherit"}}, {"dmarc_rua_mode": {"keep"}}, {"dmarc_rua_mode": {"custom"}}, {"dmarc_rua_mode": {"custom"}, "dmarc_rua_email": {"not an address"}}, {}, } { if rec := s.saveReports(form); rec.Code != http.StatusBadRequest { t.Errorf("%v = %d, want 400", form, rec.Code) } if got := s.reportAddress(t); got != "reports@hub.example" { t.Fatalf("%v changed the address to %q", form, got) } } // A refused address comes back in the field, escaped, with its choice. rec := s.saveReports(url.Values{"dmarc_rua_mode": {"custom"}, "dmarc_rua_email": {`x">@no`}}) has(t, "refusal", rec.Body.String(), `