From 39d80df80933b205269fa76482966f1f67f35ce8 Mon Sep 17 00:00:00 2001 From: Matthias Gliwka Date: Sun, 10 Aug 2025 10:34:26 +0200 Subject: [PATCH 1/2] Feature: Allow rejected SMTP recipients to be silently dropped (#549) --- cmd/root.go | 4 + config/config.go | 3 + internal/smtpd/main.go | 19 ++-- internal/smtpd/smtpd.go | 71 +++++++++------ internal/smtpd/smtpd_test.go | 169 +++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 34 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e8912cf..27a1370 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -126,6 +126,7 @@ func init() { rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain ") rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed") rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)") + rootCmd.Flags().BoolVar(&config.SMTPSilentlyDropRejectedRecipients, "smtp-silently-drop-rejected-recipients", config.SMTPSilentlyDropRejectedRecipients, "Accept emails to rejected recipients with 2xx response but silently drop them") rootCmd.Flags().BoolVar(&smtpd.DisableReverseDNS, "smtp-disable-rdns", smtpd.DisableReverseDNS, "Disable SMTP reverse DNS lookups") // SMTP relay @@ -301,6 +302,9 @@ func initConfigFromEnv() { if len(os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")) > 0 { config.SMTPAllowedRecipients = os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS") } + if getEnabledFromEnv("MP_SMTP_SILENTLY_DROP_REJECTED_RECIPIENTS") { + config.SMTPSilentlyDropRejectedRecipients = true + } if getEnabledFromEnv("MP_SMTP_DISABLE_RDNS") { smtpd.DisableReverseDNS = true } diff --git a/config/config.go b/config/config.go index f5d54cf..fcba310 100644 --- a/config/config.go +++ b/config/config.go @@ -181,6 +181,9 @@ var ( // SMTPAllowedRecipientsRegexp is the compiled version of SMTPAllowedRecipients SMTPAllowedRecipientsRegexp *regexp.Regexp + // SMTPSilentlyDropRejectedRecipients if true, will accept emails to rejected recipients with 2xx response but silently drop them + SMTPSilentlyDropRejectedRecipients bool + // POP3Listen address - if set then Mailpit will start the POP3 server and listen on this address POP3Listen = "[::]:1110" diff --git a/internal/smtpd/main.go b/internal/smtpd/main.go index ca61771..f093e8b 100644 --- a/internal/smtpd/main.go +++ b/internal/smtpd/main.go @@ -194,15 +194,16 @@ func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthHandler) Debug = true // to enable Mailpit logging srv := &Server{ - Addr: addr, - MsgIDHandler: handler, - HandlerRcpt: handlerRcpt, - AppName: "Mailpit", - Hostname: "", - AuthHandler: nil, - AuthRequired: false, - MaxRecipients: config.SMTPMaxRecipients, - DisableReverseDNS: DisableReverseDNS, + Addr: addr, + MsgIDHandler: handler, + HandlerRcpt: handlerRcpt, + AppName: "Mailpit", + Hostname: "", + AuthHandler: nil, + AuthRequired: false, + MaxRecipients: config.SMTPMaxRecipients, + SilentlyDropRejectedRecipients: config.SMTPSilentlyDropRejectedRecipients, + DisableReverseDNS: DisableReverseDNS, LogRead: func(remoteIP, verb, line string) { logger.Log().Debugf("[smtpd] %s (%s) %s", verbLogTranslator(verb), remoteIP, line) }, diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 29cb30b..4ed7223 100644 --- a/internal/smtpd/smtpd.go +++ b/internal/smtpd/smtpd.go @@ -92,26 +92,27 @@ type LogFunc func(remoteIP, verb, line string) // Server is an SMTP server. type Server struct { - Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty - AppName string - AuthHandler AuthHandler - AuthMechs map[string]bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance. - AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured. - DisableReverseDNS bool // Disable reverse DNS lookups, enforces "unknown" hostname - Handler Handler - HandlerRcpt HandlerRcpt - Hostname string - LogRead LogFunc - LogWrite LogFunc - MaxSize int // Maximum message size allowed, in bytes - MaxRecipients int // Maximum number of recipients, defaults to 100. - MsgIDHandler MsgIDHandler - Timeout time.Duration - TLSConfig *tls.Config - TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured. - TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured. - Protocol string // Default tcp, supports unix - SocketPerm fs.FileMode // if using Unix socket, socket permissions + Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty + AppName string + AuthHandler AuthHandler + AuthMechs map[string]bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance. + AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured. + DisableReverseDNS bool // Disable reverse DNS lookups, enforces "unknown" hostname + Handler Handler + HandlerRcpt HandlerRcpt + Hostname string + LogRead LogFunc + LogWrite LogFunc + MaxSize int // Maximum message size allowed, in bytes + MaxRecipients int // Maximum number of recipients, defaults to 100. + MsgIDHandler MsgIDHandler + SilentlyDropRejectedRecipients bool // Accept emails to rejected recipients with 2xx response but silently drop them + Timeout time.Duration + TLSConfig *tls.Config + TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured. + TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured. + Protocol string // Default tcp, supports unix + SocketPerm fs.FileMode // if using Unix socket, socket permissions inShutdown int32 // server was closed or shutdown openSessions int32 // count of open sessions @@ -363,6 +364,7 @@ func (s *session) serve() { var from string var gotFrom bool var to []string + var hasRejectedRecipients bool var buffer bytes.Buffer // RFC 5321 specifies support for minimum of 100 recipients is required. @@ -397,6 +399,7 @@ loop: from = "" gotFrom = false to = nil + hasRejectedRecipients = false buffer.Reset() case "EHLO": s.remoteName = args @@ -406,6 +409,7 @@ loop: from = "" gotFrom = false to = nil + hasRejectedRecipients = false buffer.Reset() case "MAIL": if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls { @@ -457,6 +461,7 @@ loop: } to = nil + hasRejectedRecipients = false buffer.Reset() case "RCPT": if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls { @@ -492,6 +497,9 @@ loop: if accept { to = append(to, match[1]) s.writef("250 2.1.5 Ok") + } else if s.srv.SilentlyDropRejectedRecipients { + hasRejectedRecipients = true + s.writef("250 2.1.5 Ok") } else { s.writef("550 5.1.0 Requested action not taken: mailbox unavailable") } @@ -506,7 +514,8 @@ loop: s.writef("530 5.7.0 Authentication required") break } - if !gotFrom || len(to) == 0 { + hasRecipients := len(to) > 0 || hasRejectedRecipients + if !gotFrom || !hasRecipients { s.writef("503 5.5.1 Bad sequence of commands (MAIL & RCPT required before DATA)") break } @@ -536,11 +545,13 @@ loop: // Create Received header & write message body into buffer. buffer.Reset() - buffer.Write(s.makeHeaders(to)) + if len(to) > 0 { + buffer.Write(s.makeHeaders(to)) + } buffer.Write(data) - // Pass mail on to handler. - if s.srv.Handler != nil { + // Pass mail on to handler only if there are valid recipients. + if len(to) > 0 && s.srv.Handler != nil { err := s.srv.Handler(s.conn.RemoteAddr(), from, to, buffer.Bytes()) if err != nil { checkErrFormat := regexp.MustCompile(`^([2-5][0-9]{2})[\s\-](.+)$`) @@ -552,7 +563,7 @@ loop: break } s.writef("250 2.0.0 Ok: queued") - } else if s.srv.MsgIDHandler != nil { + } else if len(to) > 0 && s.srv.MsgIDHandler != nil { msgID, err := s.srv.MsgIDHandler(s.conn.RemoteAddr(), from, to, buffer.Bytes(), s.username) if err != nil { checkErrFormat := regexp.MustCompile(`^([2-5][0-9]{2})[\s\-](.+)$`) @@ -570,6 +581,13 @@ loop: s.writef("250 2.0.0 Ok: queued") } } else { + if hasRejectedRecipients && Debug { + if s.srv.LogWrite != nil { + s.srv.LogWrite(s.remoteIP, "DEBUG", "Message from sender silently dropped (rejected recipients)") + } else { + log.Printf("%s DEBUG Message from sender silently dropped (rejected recipients)", s.remoteIP) + } + } s.writef("250 2.0.0 Ok: queued") } @@ -577,6 +595,7 @@ loop: from = "" gotFrom = false to = nil + hasRejectedRecipients = false buffer.Reset() case "QUIT": s.writef("221 2.0.0 %s %s ESMTP Service closing transmission channel", s.srv.Hostname, s.srv.AppName) @@ -590,6 +609,7 @@ loop: from = "" gotFrom = false to = nil + hasRejectedRecipients = false buffer.Reset() case "NOOP": s.writef("250 2.0.0 Ok") @@ -666,6 +686,7 @@ loop: from = "" gotFrom = false to = nil + hasRejectedRecipients = false buffer.Reset() case "AUTH": if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls { diff --git a/internal/smtpd/smtpd_test.go b/internal/smtpd/smtpd_test.go index 0f75482..37a9e84 100644 --- a/internal/smtpd/smtpd_test.go +++ b/internal/smtpd/smtpd_test.go @@ -1598,3 +1598,172 @@ func TestCmdShutdown(t *testing.T) { _ = conn.Close() } + +type mockDropRejectedHandler struct { + handlerCalled int + lastFrom string + lastTo []string + msgIDCalled int + lastMsgIDFrom string + lastMsgIDTo []string +} + +func (m *mockDropRejectedHandler) handler(remoteAddr net.Addr, from string, to []string, data []byte) error { + m.handlerCalled++ + m.lastFrom = from + m.lastTo = append([]string{}, to...) // copy slice + return nil +} + +func (m *mockDropRejectedHandler) msgIDHandler(remoteAddr net.Addr, from string, to []string, data []byte, username *string) (string, error) { + m.msgIDCalled++ + m.lastMsgIDFrom = from + m.lastMsgIDTo = append([]string{}, to...) // copy slice + return "test-message-id", nil +} + +// Test the SilentlyDropRejectedRecipients option +func TestSilentlyDropRejectedRecipients(t *testing.T) { + tests := []struct { + name string + silentlyDropRejectedRecipients bool + handlerRcpt func(net.Addr, string, string) bool + rcptCommands []struct{ addr, expectedCode string } + expectedHandlerCalls int + expectedHandlerRecipients []string + useMsgIDHandler bool + }{ + { + name: "Disabled_DefaultBehavior", + silentlyDropRejectedRecipients: false, + handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { + return !strings.HasSuffix(to, "@rejected.com") + }, + rcptCommands: []struct{ addr, expectedCode string }{ + {"valid@example.com", "250"}, + {"invalid@rejected.com", "550"}, + }, + expectedHandlerCalls: 1, + expectedHandlerRecipients: []string{"valid@example.com"}, + }, + { + name: "Enabled_MixedRecipients", + silentlyDropRejectedRecipients: true, + handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { + return !strings.HasSuffix(to, "@rejected.com") + }, + rcptCommands: []struct{ addr, expectedCode string }{ + {"valid1@example.com", "250"}, + {"valid2@example.com", "250"}, + {"invalid1@rejected.com", "250"}, // Now accepted but dropped + {"invalid2@rejected.com", "250"}, // Now accepted but dropped + }, + expectedHandlerCalls: 1, + expectedHandlerRecipients: []string{"valid1@example.com", "valid2@example.com"}, + }, + { + name: "Enabled_AllRejected", + silentlyDropRejectedRecipients: true, + handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { + return false // Reject all + }, + rcptCommands: []struct{ addr, expectedCode string }{ + {"test1@example.com", "250"}, // Accepted but dropped + {"test2@example.com", "250"}, // Accepted but dropped + }, + expectedHandlerCalls: 0, // No handler calls since all rejected + expectedHandlerRecipients: nil, + }, + { + name: "Enabled_OnlyValid", + silentlyDropRejectedRecipients: true, + handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { + return strings.HasSuffix(to, "@valid.com") + }, + rcptCommands: []struct{ addr, expectedCode string }{ + {"user1@valid.com", "250"}, + {"user2@valid.com", "250"}, + {"user3@valid.com", "250"}, + }, + expectedHandlerCalls: 1, + expectedHandlerRecipients: []string{"user1@valid.com", "user2@valid.com", "user3@valid.com"}, + }, + { + name: "Enabled_WithMsgIDHandler", + silentlyDropRejectedRecipients: true, + handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { + return !strings.HasSuffix(to, "@rejected.com") + }, + rcptCommands: []struct{ addr, expectedCode string }{ + {"valid@example.com", "250"}, + {"invalid@rejected.com", "250"}, // Accepted but dropped + }, + expectedHandlerCalls: 1, + expectedHandlerRecipients: []string{"valid@example.com"}, + useMsgIDHandler: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockDropRejectedHandler{} + + server := &Server{ + Hostname: "mail.example.com", + AppName: "TestMail", + MaxRecipients: 100, + HandlerRcpt: tt.handlerRcpt, + SilentlyDropRejectedRecipients: tt.silentlyDropRejectedRecipients, + } + + if tt.useMsgIDHandler { + server.MsgIDHandler = mock.msgIDHandler + } else { + server.Handler = mock.handler + } + + conn := newConn(t, server) + defer func() { _ = conn.Close() }() + + cmdCode(t, conn, "HELO host.example.com", "250") + cmdCode(t, conn, "MAIL FROM:", "250") + + // Send RCPT commands + for _, rcpt := range tt.rcptCommands { + cmdCode(t, conn, "RCPT TO:<"+rcpt.addr+">", rcpt.expectedCode) + } + + // Send DATA + cmdCode(t, conn, "DATA", "354") + cmdCode(t, conn, "Subject: Test\r\n\r\nTest message\r\n.", "250") + cmdCode(t, conn, "QUIT", "221") + + // Verify handler calls + if tt.useMsgIDHandler { + if mock.msgIDCalled != tt.expectedHandlerCalls { + t.Errorf("Expected %d MsgIDHandler calls, got %d", tt.expectedHandlerCalls, mock.msgIDCalled) + } + if tt.expectedHandlerCalls > 0 { + if mock.lastMsgIDFrom != "sender@example.com" { + t.Errorf("Expected from 'sender@example.com', got '%s'", mock.lastMsgIDFrom) + } + if !reflect.DeepEqual(mock.lastMsgIDTo, tt.expectedHandlerRecipients) { + t.Errorf("Expected recipients %v, got %v", tt.expectedHandlerRecipients, mock.lastMsgIDTo) + } + } + } else { + if mock.handlerCalled != tt.expectedHandlerCalls { + t.Errorf("Expected %d handler calls, got %d", tt.expectedHandlerCalls, mock.handlerCalled) + } + if tt.expectedHandlerCalls > 0 { + if mock.lastFrom != "sender@example.com" { + t.Errorf("Expected from 'sender@example.com', got '%s'", mock.lastFrom) + } + if !reflect.DeepEqual(mock.lastTo, tt.expectedHandlerRecipients) { + t.Errorf("Expected recipients %v, got %v", tt.expectedHandlerRecipients, mock.lastTo) + } + } + } + }) + } +} From 41ef4ecd60a0313b35176d902c593f1eae85dba8 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 10 Aug 2025 21:04:22 +1200 Subject: [PATCH 2/2] Rename smtp-silently-drop-rejected-recipients to smtp-ignore-rejected-recipients --- cmd/root.go | 6 ++--- config/config.go | 12 +++++++-- internal/smtpd/main.go | 20 +++++++-------- internal/smtpd/smtpd.go | 44 ++++++++++++++++----------------- internal/smtpd/smtpd_test.go | 48 ++++++++++++++++++------------------ 5 files changed, 69 insertions(+), 61 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 27a1370..082ca57 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -126,7 +126,7 @@ func init() { rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain ") rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed") rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)") - rootCmd.Flags().BoolVar(&config.SMTPSilentlyDropRejectedRecipients, "smtp-silently-drop-rejected-recipients", config.SMTPSilentlyDropRejectedRecipients, "Accept emails to rejected recipients with 2xx response but silently drop them") + rootCmd.Flags().BoolVar(&config.SMTPIgnoreRejectedRecipients, "smtp-ignore-rejected-recipients", config.SMTPIgnoreRejectedRecipients, "Ignore rejected SMTP recipients with 2xx response") rootCmd.Flags().BoolVar(&smtpd.DisableReverseDNS, "smtp-disable-rdns", smtpd.DisableReverseDNS, "Disable SMTP reverse DNS lookups") // SMTP relay @@ -302,8 +302,8 @@ func initConfigFromEnv() { if len(os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")) > 0 { config.SMTPAllowedRecipients = os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS") } - if getEnabledFromEnv("MP_SMTP_SILENTLY_DROP_REJECTED_RECIPIENTS") { - config.SMTPSilentlyDropRejectedRecipients = true + if getEnabledFromEnv("MP_SMTP_IGNORE_REJECTED_RECIPIENTS") { + config.SMTPIgnoreRejectedRecipients = true } if getEnabledFromEnv("MP_SMTP_DISABLE_RDNS") { smtpd.DisableReverseDNS = true diff --git a/config/config.go b/config/config.go index fcba310..4a6a91b 100644 --- a/config/config.go +++ b/config/config.go @@ -181,8 +181,8 @@ var ( // SMTPAllowedRecipientsRegexp is the compiled version of SMTPAllowedRecipients SMTPAllowedRecipientsRegexp *regexp.Regexp - // SMTPSilentlyDropRejectedRecipients if true, will accept emails to rejected recipients with 2xx response but silently drop them - SMTPSilentlyDropRejectedRecipients bool + // SMTPIgnoreRejectedRecipients if true, will accept emails to rejected recipients with 2xx response but silently drop them + SMTPIgnoreRejectedRecipients bool // POP3Listen address - if set then Mailpit will start the POP3 server and listen on this address POP3Listen = "[::]:1110" @@ -584,6 +584,14 @@ func VerifyConfig() error { logger.Log().Infof("[smtp] only allowing recipients matching regexp: %s", SMTPAllowedRecipients) } + if SMTPIgnoreRejectedRecipients { + if SMTPAllowedRecipientsRegexp == nil { + logger.Log().Warn("[smtp] ignoring rejected recipients has no effect without setting smtp-allowed-recipients") + } else { + logger.Log().Info("[smtp] ignoring rejected recipients") + } + } + if err := parseRelayConfig(SMTPRelayConfigFile); err != nil { return err } diff --git a/internal/smtpd/main.go b/internal/smtpd/main.go index f093e8b..cec013c 100644 --- a/internal/smtpd/main.go +++ b/internal/smtpd/main.go @@ -194,16 +194,16 @@ func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthHandler) Debug = true // to enable Mailpit logging srv := &Server{ - Addr: addr, - MsgIDHandler: handler, - HandlerRcpt: handlerRcpt, - AppName: "Mailpit", - Hostname: "", - AuthHandler: nil, - AuthRequired: false, - MaxRecipients: config.SMTPMaxRecipients, - SilentlyDropRejectedRecipients: config.SMTPSilentlyDropRejectedRecipients, - DisableReverseDNS: DisableReverseDNS, + Addr: addr, + MsgIDHandler: handler, + HandlerRcpt: handlerRcpt, + AppName: "Mailpit", + Hostname: "", + AuthHandler: nil, + AuthRequired: false, + MaxRecipients: config.SMTPMaxRecipients, + IgnoreRejectedRecipients: config.SMTPIgnoreRejectedRecipients, + DisableReverseDNS: DisableReverseDNS, LogRead: func(remoteIP, verb, line string) { logger.Log().Debugf("[smtpd] %s (%s) %s", verbLogTranslator(verb), remoteIP, line) }, diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 4ed7223..da1b816 100644 --- a/internal/smtpd/smtpd.go +++ b/internal/smtpd/smtpd.go @@ -92,27 +92,27 @@ type LogFunc func(remoteIP, verb, line string) // Server is an SMTP server. type Server struct { - Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty - AppName string - AuthHandler AuthHandler - AuthMechs map[string]bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance. - AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured. - DisableReverseDNS bool // Disable reverse DNS lookups, enforces "unknown" hostname - Handler Handler - HandlerRcpt HandlerRcpt - Hostname string - LogRead LogFunc - LogWrite LogFunc - MaxSize int // Maximum message size allowed, in bytes - MaxRecipients int // Maximum number of recipients, defaults to 100. - MsgIDHandler MsgIDHandler - SilentlyDropRejectedRecipients bool // Accept emails to rejected recipients with 2xx response but silently drop them - Timeout time.Duration - TLSConfig *tls.Config - TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured. - TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured. - Protocol string // Default tcp, supports unix - SocketPerm fs.FileMode // if using Unix socket, socket permissions + Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty + AppName string + AuthHandler AuthHandler + AuthMechs map[string]bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance. + AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured. + DisableReverseDNS bool // Disable reverse DNS lookups, enforces "unknown" hostname + Handler Handler + HandlerRcpt HandlerRcpt + Hostname string + LogRead LogFunc + LogWrite LogFunc + MaxSize int // Maximum message size allowed, in bytes + MaxRecipients int // Maximum number of recipients, defaults to 100. + MsgIDHandler MsgIDHandler + IgnoreRejectedRecipients bool // Accept emails to rejected recipients with 2xx response but silently drop them + Timeout time.Duration + TLSConfig *tls.Config + TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured. + TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured. + Protocol string // Default tcp, supports unix + SocketPerm fs.FileMode // if using Unix socket, socket permissions inShutdown int32 // server was closed or shutdown openSessions int32 // count of open sessions @@ -497,7 +497,7 @@ loop: if accept { to = append(to, match[1]) s.writef("250 2.1.5 Ok") - } else if s.srv.SilentlyDropRejectedRecipients { + } else if s.srv.IgnoreRejectedRecipients { hasRejectedRecipients = true s.writef("250 2.1.5 Ok") } else { diff --git a/internal/smtpd/smtpd_test.go b/internal/smtpd/smtpd_test.go index 37a9e84..63b5219 100644 --- a/internal/smtpd/smtpd_test.go +++ b/internal/smtpd/smtpd_test.go @@ -1622,20 +1622,20 @@ func (m *mockDropRejectedHandler) msgIDHandler(remoteAddr net.Addr, from string, return "test-message-id", nil } -// Test the SilentlyDropRejectedRecipients option -func TestSilentlyDropRejectedRecipients(t *testing.T) { +// Test the IgnoreRejectedRecipients option +func TestIgnoreRejectedRecipients(t *testing.T) { tests := []struct { - name string - silentlyDropRejectedRecipients bool - handlerRcpt func(net.Addr, string, string) bool - rcptCommands []struct{ addr, expectedCode string } - expectedHandlerCalls int - expectedHandlerRecipients []string - useMsgIDHandler bool + name string + IgnoreRejectedRecipients bool + handlerRcpt func(net.Addr, string, string) bool + rcptCommands []struct{ addr, expectedCode string } + expectedHandlerCalls int + expectedHandlerRecipients []string + useMsgIDHandler bool }{ { - name: "Disabled_DefaultBehavior", - silentlyDropRejectedRecipients: false, + name: "Disabled_DefaultBehavior", + IgnoreRejectedRecipients: false, handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { return !strings.HasSuffix(to, "@rejected.com") }, @@ -1647,8 +1647,8 @@ func TestSilentlyDropRejectedRecipients(t *testing.T) { expectedHandlerRecipients: []string{"valid@example.com"}, }, { - name: "Enabled_MixedRecipients", - silentlyDropRejectedRecipients: true, + name: "Enabled_MixedRecipients", + IgnoreRejectedRecipients: true, handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { return !strings.HasSuffix(to, "@rejected.com") }, @@ -1662,8 +1662,8 @@ func TestSilentlyDropRejectedRecipients(t *testing.T) { expectedHandlerRecipients: []string{"valid1@example.com", "valid2@example.com"}, }, { - name: "Enabled_AllRejected", - silentlyDropRejectedRecipients: true, + name: "Enabled_AllRejected", + IgnoreRejectedRecipients: true, handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { return false // Reject all }, @@ -1675,8 +1675,8 @@ func TestSilentlyDropRejectedRecipients(t *testing.T) { expectedHandlerRecipients: nil, }, { - name: "Enabled_OnlyValid", - silentlyDropRejectedRecipients: true, + name: "Enabled_OnlyValid", + IgnoreRejectedRecipients: true, handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { return strings.HasSuffix(to, "@valid.com") }, @@ -1689,8 +1689,8 @@ func TestSilentlyDropRejectedRecipients(t *testing.T) { expectedHandlerRecipients: []string{"user1@valid.com", "user2@valid.com", "user3@valid.com"}, }, { - name: "Enabled_WithMsgIDHandler", - silentlyDropRejectedRecipients: true, + name: "Enabled_WithMsgIDHandler", + IgnoreRejectedRecipients: true, handlerRcpt: func(remoteAddr net.Addr, from string, to string) bool { return !strings.HasSuffix(to, "@rejected.com") }, @@ -1709,11 +1709,11 @@ func TestSilentlyDropRejectedRecipients(t *testing.T) { mock := &mockDropRejectedHandler{} server := &Server{ - Hostname: "mail.example.com", - AppName: "TestMail", - MaxRecipients: 100, - HandlerRcpt: tt.handlerRcpt, - SilentlyDropRejectedRecipients: tt.silentlyDropRejectedRecipients, + Hostname: "mail.example.com", + AppName: "TestMail", + MaxRecipients: 100, + HandlerRcpt: tt.handlerRcpt, + IgnoreRejectedRecipients: tt.IgnoreRejectedRecipients, } if tt.useMsgIDHandler {