Files
Hammerhead/mailegress.go
T

177 lines
5.1 KiB
Go

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"`
Findings []Finding `json:"findings"` // the deliverability checklist, with fixes
}
// 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
v.Findings = s.mailEgressFindingsLocked(e, chk)
}
if v.Findings == nil {
v.Findings = []Finding{}
}
out = append(out, v)
}
return out
}