From 2cd0357af01d6e69f027ec327e0d1e88b220d767 Mon Sep 17 00:00:00 2001 From: Steffen Heil | secforge Date: Sun, 10 Aug 2025 20:51:26 +0200 Subject: [PATCH] Add email blacklist functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Blacklist map[string]bool to Config struct for storing blacklisted emails - Add APIv2 endpoints: GET/POST/DELETE /api/v2/blacklist/{email} - Add blacklist parameter to Accept() function for SMTP validation - Reject emails to/from blacklisted addresses with 550 Invalid recipient/sender 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- api/v2.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ config/config.go | 2 ++ smtp/session.go | 22 +++++++++++----- smtp/smtp.go | 5 +--- 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/api/v2.go b/api/v2.go index 4ef0c40..1ea5e1d 100644 --- a/api/v2.go +++ b/api/v2.go @@ -46,6 +46,13 @@ func createAPIv2(conf *config.Config, r *pat.Router) *APIv2 { r.Path(conf.WebPath + "/api/v2/outgoing-smtp").Methods("GET").HandlerFunc(apiv2.listOutgoingSMTP) r.Path(conf.WebPath + "/api/v2/outgoing-smtp").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions) + r.Path(conf.WebPath + "/api/v2/blacklist").Methods("GET").HandlerFunc(apiv2.blacklist_list) + r.Path(conf.WebPath + "/api/v2/blacklist").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions) + + r.Path(conf.WebPath + "/api/v2/blacklist/{email}").Methods("POST").HandlerFunc(apiv2.blacklist_add) + r.Path(conf.WebPath + "/api/v2/blacklist/{email}").Methods("DELETE").HandlerFunc(apiv2.blacklist_remove) + r.Path(conf.WebPath + "/api/v2/blacklist/{email}").Methods("OPTIONS").HandlerFunc(apiv2.defaultOptions) + r.Path(conf.WebPath + "/api/v2/websocket").Methods("GET").HandlerFunc(apiv2.websocket) go func() { @@ -256,3 +263,64 @@ func (apiv2 *APIv2) broadcast(msg *data.Message) { apiv2.wsHub.Broadcast(msg) } + +func (apiv2 *APIv2) blacklist_list(w http.ResponseWriter, req *http.Request) { + log.Println("[APIv2] GET /api/v2/blacklist") + + apiv2.defaultOptions(w, req) + + emails := make([]string, 0, len(apiv2.config.Blacklist)) + for email := range apiv2.config.Blacklist { + emails = append(emails, email) + } + + bytes, err := json.Marshal(emails) + if err != nil { + log.Printf("Error marshaling blacklist: %s", err) + w.WriteHeader(500) + return + } + + w.Header().Add("Content-Type", "application/json") + w.Write(bytes) +} + +func (apiv2 *APIv2) blacklist_add(w http.ResponseWriter, req *http.Request) { + email := req.URL.Query().Get(":email") + log.Printf("[APIv2] POST /api/v2/blacklist/%s", email) + + apiv2.defaultOptions(w, req) + + if email == "" { + w.WriteHeader(400) + w.Write([]byte("Email address is required")) + return + } + + apiv2.config.Blacklist[email] = true + log.Printf("Added %s to blacklist", email) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(200) + w.Write([]byte(`{"success": true}`)) +} + +func (apiv2 *APIv2) blacklist_remove(w http.ResponseWriter, req *http.Request) { + email := req.URL.Query().Get(":email") + log.Printf("[APIv2] DELETE /api/v2/blacklist/%s", email) + + apiv2.defaultOptions(w, req) + + if email == "" { + w.WriteHeader(400) + w.Write([]byte("Email address is required")) + return + } + + delete(apiv2.config.Blacklist, email) + log.Printf("Removed %s from blacklist", email) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(200) + w.Write([]byte(`{"success": true}`)) +} diff --git a/config/config.go b/config/config.go index 59cb706..b329b2c 100644 --- a/config/config.go +++ b/config/config.go @@ -27,6 +27,7 @@ func DefaultConfig() *Config { WebPath: "", MessageChan: make(chan *data.Message), OutgoingSMTP: make(map[string]*OutgoingSMTP), + Blacklist: make(map[string]bool), } } @@ -49,6 +50,7 @@ type Config struct { OutgoingSMTPFile string OutgoingSMTP map[string]*OutgoingSMTP WebPath string + Blacklist map[string]bool } // OutgoingSMTP is an outgoing SMTP server config diff --git a/smtp/session.go b/smtp/session.go index 10a220d..caae2a4 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/ian-kent/linkio" + "github.com/mailhog/MailHog-Server/config" "github.com/mailhog/MailHog-Server/monkey" "github.com/mailhog/data" "github.com/mailhog/smtp" @@ -24,6 +25,7 @@ type Session struct { isTLS bool line string link *linkio.Link + blacklist map[string]bool reader io.Reader writer io.Writer @@ -31,16 +33,16 @@ type Session struct { } // Accept starts a new SMTP session using io.ReadWriteCloser -func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string, monkey monkey.ChaosMonkey) { +func Accept(remoteAddress string, conn io.ReadWriteCloser, cfg *config.Config) { defer conn.Close() proto := smtp.NewProtocol() - proto.Hostname = hostname + proto.Hostname = cfg.Hostname var link *linkio.Link reader := io.Reader(conn) writer := io.Writer(conn) - if monkey != nil { - linkSpeed := monkey.LinkSpeed() + if cfg.Monkey != nil { + linkSpeed := cfg.Monkey.LinkSpeed() if linkSpeed != nil { link = linkio.NewLink(*linkSpeed * linkio.BytePerSecond) reader = link.NewLinkReader(io.Reader(conn)) @@ -48,7 +50,7 @@ func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Stora } } - session := &Session{conn, proto, storage, messageChan, remoteAddress, false, "", link, reader, writer, monkey} + session := &Session{conn, proto, cfg.Storage, cfg.MessageChan, remoteAddress, false, "", link, cfg.Blacklist, reader, writer, cfg.Monkey} proto.LogHandler = session.logf proto.MessageReceivedHandler = session.acceptMessage proto.ValidateSenderHandler = session.validateSender @@ -59,7 +61,7 @@ func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Stora session.logf("Starting session") session.Write(proto.Start()) for session.Read() == true { - if monkey != nil && monkey.Disconnect != nil && monkey.Disconnect() { + if cfg.Monkey != nil && cfg.Monkey.Disconnect != nil && cfg.Monkey.Disconnect() { session.conn.Close() break } @@ -79,6 +81,10 @@ func (c *Session) validateAuthentication(mechanism string, args ...string) (erro } func (c *Session) validateRecipient(to string) bool { + if c.blacklist != nil && c.blacklist[to] { + c.logf("Rejecting email to blacklisted address: %s", to) + return false + } if c.monkey != nil { ok := c.monkey.ValidRCPT(to) if !ok { @@ -89,6 +95,10 @@ func (c *Session) validateRecipient(to string) bool { } func (c *Session) validateSender(from string) bool { + if c.blacklist != nil && c.blacklist[from] { + c.logf("Rejecting email from blacklisted address: %s", from) + return false + } if c.monkey != nil { ok := c.monkey.ValidMAIL(from) if !ok { diff --git a/smtp/smtp.go b/smtp/smtp.go index 38a9b51..ec80e9a 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -34,10 +34,7 @@ func Listen(cfg *config.Config, exitCh chan int) *net.TCPListener { go Accept( conn.(*net.TCPConn).RemoteAddr().String(), io.ReadWriteCloser(conn), - cfg.Storage, - cfg.MessageChan, - cfg.Hostname, - cfg.Monkey, + cfg, ) } }