package hammerhead import ( "errors" "net" "net/http" "strings" "git.veillax.com/Veillax/reef/model" ) const defaultMailListen = "127.0.0.1:2525" func (s *Server) findMailEgress(id string) *model.MailEgress { for _, e := range s.st.MailEgress { if e.ID == id { return e } } return nil } func isLoopbackListen(addr string) bool { host, _, err := net.SplitHostPort(addr) if err != nil { return false } if strings.EqualFold(host, "localhost") { return true } ip := net.ParseIP(host) return ip != nil && ip.IsLoopback() } // validateMailEgress checks e against org c and normalises it. Caller holds s.mu. func (s *Server) validateMailEgress(c *orgCtx, e *model.MailEgress) error { e.OrgID = c.Org.ID e.Name = strings.TrimSpace(e.Name) site := s.findSite(e.SiteID) if site == nil || site.Local || site.OrgID != c.Org.ID { return errors.New("choose one of your sites: the mail server's network (the built-in local site already sends from the edge itself)") } edge := s.findEdge(e.EdgeID) if edge == nil || !s.edgeAllowedFor(edge, c.Org.ID) { return errors.New("choose an edge") } e.Listen = strings.TrimSpace(e.Listen) if e.Listen == "" { e.Listen = defaultMailListen } if !validAddr(e.Listen) { return errors.New("listen address must look like 127.0.0.1:2525 (an address on the site's machine)") } for _, o := range s.st.MailEgress { if o.ID != e.ID && o.SiteID == e.SiteID && o.Listen == e.Listen { return errors.New("another mail egress on that site already listens on " + e.Listen) } } ip := net.ParseIP(strings.TrimSpace(e.SourceIP)) if ip == nil || ip.IsUnspecified() { return errors.New("choose the edge IP address your mail should leave from") } e.SourceIP = ip.String() e.HeloName = strings.ToLower(strings.TrimSpace(e.HeloName)) if !validHostname(e.HeloName) || strings.HasPrefix(e.HeloName, "*.") { return errors.New("enter the hostname your mail server announces, like mail.example.com (its reverse DNS should match)") } e.Smarthost = strings.TrimSpace(e.Smarthost) if e.Smarthost != "" && !validAddr(e.Smarthost) { return errors.New("smarthost must look like smtp.example.com:587") } if e.RatePerMinute < 0 || e.RatePerMinute > 6000 { return errors.New("messages per minute must be between 1 and 6000") } if e.Username != "" && e.Password == "" { return errors.New("set a password too, or clear the username") } if e.Username == "" && !isLoopbackListen(e.Listen) { return errors.New("listening beyond this machine without a password would let anything on your network send mail from your IP; set a username and password, or listen on 127.0.0.1") } if e.Name == "" { e.Name = "mail via " + e.HeloName } return nil } func (s *Server) saveMailEgress(w http.ResponseWriter, r *http.Request, c *orgCtx) { var in model.MailEgress if !decode(w, r, &in) { return } s.mu.Lock() defer s.mu.Unlock() id := r.PathValue("id") idx := -1 if id != "" { for i, e := range s.st.MailEgress { if e.ID == id && e.OrgID == c.Org.ID { idx = i } } if idx < 0 { fail(w, 404, errors.New("no such mail egress")) return } in.ID = id if in.Password == "" && in.Username != "" && in.Username == s.st.MailEgress[idx].Username { in.Password = s.st.MailEgress[idx].Password // blank keeps the saved password } } if err := s.validateMailEgress(c, &in); err != nil { fail(w, 400, err) return } if idx >= 0 { s.st.MailEgress[idx] = &in delete(s.mailStatus, id) } else { in.ID = newID() s.st.MailEgress = append(s.st.MailEgress, &in) } s.bump() s.audit(c.Org.ID, c.Actor, "mailegress.save", in.Name) resp := in // the stored record keeps its password; the response never carries it resp.Password = "" writeJSON(w, 200, resp) } func (s *Server) deleteMailEgress(w http.ResponseWriter, r *http.Request, c *orgCtx) { id := r.PathValue("id") s.mu.Lock() defer s.mu.Unlock() e := s.findMailEgress(id) if e == nil || e.OrgID != c.Org.ID { fail(w, 404, errors.New("no such mail egress")) return } s.st.MailEgress = filter(s.st.MailEgress, func(x *model.MailEgress) bool { return x.ID != id }) delete(s.mailStatus, id) delete(s.mailChecks, id) s.bump() s.audit(c.Org.ID, c.Actor, "mailegress.delete", e.Name) ok(w) } type mailEgressView struct { model.MailEgress PasswordSet bool `json:"passwordSet"` Status string `json:"status"` StatusKnown bool `json:"statusKnown"` Check *model.MailCheck `json:"check,omitempty"` } // mailEgressViewsLocked lists an org's mail egress without secrets. Caller holds s.mu. func (s *Server) mailEgressViewsLocked(orgID string) []mailEgressView { out := []mailEgressView{} for _, e := range s.st.MailEgress { if e.OrgID != orgID { continue } v := mailEgressView{MailEgress: *e, PasswordSet: e.Password != ""} v.Password = "" v.Status, v.StatusKnown = s.mailStatus[e.ID] if chk, ok := s.mailChecks[e.ID]; ok { c := chk v.Check = &c } out = append(out, v) } return out }