diff --git a/external/cmd/signup/main.go b/external/cmd/signup/main.go index 67e3caa..b55343a 100644 --- a/external/cmd/signup/main.go +++ b/external/cmd/signup/main.go @@ -3,11 +3,15 @@ package main import ( "bytes" "database/sql" + "errors" "fmt" "io" "log" + "net" "os" "path" + "regexp" + "slices" "strings" "time" @@ -110,6 +114,39 @@ func (c *character) Say(msg string) string { strings.TrimSpace(msg)) } +var ErrNoSuchDomain = errors.New("no host found for email address") +var ErrNoSuchMailserver = errors.New("no mail server found for email address") + +// DigMX does some grubbing around to attempt to find valid email hosts, and +// then runs then through [net.LookupMX] and returns their mailserver domains. +// may return [ErrNoSuchDomain] or [ErrNoSuchMailserver]. +func DigMX(raw string) (domains []string, err error) { + re := regexp.MustCompile(`@[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\b`) // good enough + candidates := re.FindAllString(raw, -1) + + // the error checking tries to be very generous: if anything comes up + // positive we will throw no errors and just assume the rest was a fluke. + ok := false + for _, host := range candidates { + records, e := net.LookupMX(host[1:]) + if e != nil { + err = ErrNoSuchDomain + } else if len(records) == 0 { + err = ErrNoSuchMailserver + } else { + ok = true + for _, record := range records { + domains = append(domains, record.Host) + } + } + } + + if ok { + return domains, nil + } + return +} + func main() { logFile := path.Join(logDir, fmt.Sprintf("%d", time.Now().Unix())) logF, err := os.Create(logFile) @@ -201,9 +238,32 @@ func _main(l *log.Logger, db *sql.DB) error { `), "i'm sorry, before going further could you share an email with me?", newCharacter("wire guy", "a lil homonculus made of discarded computer cables"), - func(s *scene) { su.Email = string(s.Input.Bytes()) }, + func(s *scene) { + su.Email = string(s.Input.Bytes()) + suspiciousHosts, err := models.SuspiciousHosts(db) + if err != nil { + // XXX: maybe log somewhere that the database failed + return + } + var shDomains []string + for _, host := range suspiciousHosts { + shDomains = append(shDomains, host.Domain) + } + if records, err := DigMX(su.Email); err == nil { + for _, record := range records { + if slices.Contains(shDomains, record) { + su.Notes = append(su.Notes, models.SignupNote{ + Author: "dns", + Content: fmt.Sprintf("email address has suspicious host %s", record), + SignupID: su.ID, + }) + } + } + } + }, func(s *scene, tv *tview.TextView, msg string) { // TODO could check and see if it's email shaped and admonish if not + // NOTE(nbsp): DigMX call can see if email is invalid but this isn't used yet trimmed := strings.TrimSpace(msg) fmt.Fprintln(tv, s.Host.Say(fmt.Sprintf("I heard '%s'. Is that right? if so, /nod", trimmed))) }), diff --git a/models/models.go b/models/models.go index ffce1c6..a1b9b35 100644 --- a/models/models.go +++ b/models/models.go @@ -193,3 +193,35 @@ func (s *TownSignup) All(db *sql.DB) ([]*TownSignup, error) { return out, nil } + +type SuspiciousHost struct { + ID int64 + Domain string + CommonName string + Tier int64 +} + +func SuspiciousHosts(db *sql.DB) ([]SuspiciousHost, error) { + rows, err := db.Query(`SELECT id, domain, common_name, tier FROM suspicious_hosts`) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []SuspiciousHost{} + for rows.Next() { + sh := SuspiciousHost{} + if err = rows.Scan( + &sh.ID, + &sh.Domain, + &sh.CommonName, + &sh.Tier, + ); err != nil { + return nil, err + } + + out = append(out, sh) + } + + return out, nil +} diff --git a/sql/create_signups_db.sql b/sql/create_signups_db.sql index b32f8eb..112bb31 100644 --- a/sql/create_signups_db.sql +++ b/sql/create_signups_db.sql @@ -24,3 +24,13 @@ CREATE TABLE IF NOT EXISTS notes ( FOREIGN KEY (signupid) REFERENCES signups(signupid) ); + +-- 2025-11-22: bad hosts +CREATE TABLE IF NOT EXISTS suspicious_hosts ( + id INTEGER PRIMARY KEY, + domain TEXT, + + -- unused but worth adding instead of another migration later + common_name TEXT, + tier INTEGER, +)