//go:build standalone package checker import ( "errors" "net/http" "strconv" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // RenderForm implements server.Interactive: the human-facing form // exposed at GET /check when the checker runs as a standalone binary. func (p *smtpProvider) RenderForm() []sdk.CheckerOptionField { return []sdk.CheckerOptionField{ { Id: "domain", Type: "string", Label: "Domain", Placeholder: "example.com", Required: true, Description: "The email domain to probe. MX records are looked up live.", }, { Id: "helo_name", Type: "string", Label: "EHLO hostname", Placeholder: defaultEHLOName, Default: defaultEHLOName, Description: "The hostname announced in EHLO/HELO. Use a name that resolves and has a valid PTR.", }, { Id: "timeout", Type: "number", Label: "Per-endpoint timeout (seconds)", Default: 12, }, { Id: "test_null_sender", Type: "bool", Label: "Probe null sender (MAIL FROM:<>)", Default: true, }, { Id: "test_postmaster", Type: "bool", Label: "Probe RCPT TO:", Default: true, }, { Id: "test_open_relay", Type: "bool", Label: "Probe open-relay posture", Default: true, }, { Id: "test_probe_address", Type: "string", Label: "Open-relay probe recipient", Placeholder: "postmaster@example.com", Default: "postmaster@example.com", }, } } // ParseForm implements server.Interactive: turns the submitted HTML // form into the CheckerOptions that Collect expects. No AutoFill is // performed by a host here; Collect falls back to a live MX lookup when // no "service" payload is supplied, so forwarding the bare domain is // enough. func (p *smtpProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { domain := strings.TrimSpace(r.FormValue("domain")) domain = strings.TrimSuffix(domain, ".") if domain == "" { return nil, errors.New("domain is required") } if !isValidHostname(domain) { return nil, errors.New("invalid domain") } opts := sdk.CheckerOptions{ "domain": domain, } if helo := strings.TrimSpace(r.FormValue("helo_name")); helo != "" { if !isValidHostname(helo) { return nil, errors.New("invalid helo_name") } opts["helo_name"] = helo } if raw := strings.TrimSpace(r.FormValue("timeout")); raw != "" { v, err := strconv.ParseFloat(raw, 64) if err != nil { return nil, errors.New("timeout must be a number") } opts["timeout"] = v } opts["test_null_sender"] = parseBool(r, "test_null_sender", true) opts["test_postmaster"] = parseBool(r, "test_postmaster", true) opts["test_open_relay"] = parseBool(r, "test_open_relay", true) if probe := strings.TrimSpace(r.FormValue("test_probe_address")); probe != "" { if !isValidMailbox(probe) { return nil, errors.New("invalid test_probe_address") } opts["test_probe_address"] = probe } return opts, nil } // parseBool reads a checkbox-style field. HTML forms omit unchecked // checkboxes entirely, so a missing key means false, but only if the // form was actually submitted (presence of the sentinel); we use the // default when the field is not present at all. func parseBool(r *http.Request, key string, def bool) bool { if _, ok := r.Form[key]; !ok { // When the form has been parsed and _no_ checkbox was checked, // we still want false rather than the default. Detect a // submitted form by the presence of the required "domain" key. if _, submitted := r.Form["domain"]; submitted { return false } return def } v := strings.ToLower(strings.TrimSpace(r.FormValue(key))) switch v { case "", "0", "false", "off", "no": return false default: return true } }