diff --git a/cmd/inbucket/main.go b/cmd/inbucket/main.go index 834c05c..88ce6a6 100644 --- a/cmd/inbucket/main.go +++ b/cmd/inbucket/main.go @@ -145,9 +145,13 @@ signalLoop: // Wait for active connections to finish. go timedExit(*pidfile) + log.Debug().Str("phase", "shutdown").Msg("Draining SMTP connections") services.SMTPServer.Drain() + log.Debug().Str("phase", "shutdown").Msg("Draining POP3 connections") services.POP3Server.Drain() + log.Debug().Str("phase", "shutdown").Msg("Checking retention scanner is stopped") services.RetentionScanner.Join() + removePIDFile(*pidfile) closeLog() } diff --git a/pkg/server/smtp/handler_test.go b/pkg/server/smtp/handler_test.go index e1f67e1..caf2968 100644 --- a/pkg/server/smtp/handler_test.go +++ b/pkg/server/smtp/handler_test.go @@ -22,14 +22,44 @@ type scriptStep struct { expect int } -// Test commands in GREET state +// Test valid commands in GREET state. +func TestGreetStateValidCommands(t *testing.T) { + ds := test.NewStore() + server := setupSMTPServer(ds) + + tests := []scriptStep{ + {"HELO mydomain", 250}, + {"HELO mydom.com", 250}, + {"HelO mydom.com", 250}, + {"helo 127.0.0.1", 250}, + {"HELO ABC", 250}, + {"EHLO mydomain", 250}, + {"EHLO mydom.com", 250}, + {"EhlO mydom.com", 250}, + {"ehlo 127.0.0.1", 250}, + {"EHLO a", 250}, + } + + for _, tc := range tests { + t.Run(tc.send, func(t *testing.T) { + defer server.Drain() // Required to prevent test logging data race. + script := []scriptStep{ + tc, + {"QUIT", 221}} + if err := playSession(t, server, script); err != nil { + t.Error(err) + } + }) + } +} + +// Test invalid commands in GREET state. func TestGreetState(t *testing.T) { ds := test.NewStore() server := setupSMTPServer(ds) defer server.Drain() // Required to prevent test logging data race. - // Test out some mangled HELOs - script := []scriptStep{ + tests := []scriptStep{ {"HELO", 501}, {"EHLO", 501}, {"HELLO", 500}, @@ -37,47 +67,20 @@ func TestGreetState(t *testing.T) { {"hello", 500}, {"Outlook", 500}, } - if err := playSession(t, server, script); err != nil { - t.Error(err) - } - // Valid HELOs - if err := playSession(t, server, []scriptStep{{"HELO mydomain", 250}}); err != nil { - t.Error(err) + for _, tc := range tests { + t.Run(tc.send, func(t *testing.T) { + defer server.Drain() // Required to prevent test logging data race. + script := []scriptStep{ + tc, + {"QUIT", 221}} + if err := playSession(t, server, script); err != nil { + t.Error(err) + } + }) } - if err := playSession(t, server, []scriptStep{{"HELO mydom.com", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"HelO mydom.com", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"helo 127.0.0.1", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"HELO ABC", 250}}); err != nil { - t.Error(err) - } - - // Valid EHLOs - if err := playSession(t, server, []scriptStep{{"EHLO mydomain", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"EHLO mydom.com", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"EhlO mydom.com", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"ehlo 127.0.0.1", 250}}); err != nil { - t.Error(err) - } - if err := playSession(t, server, []scriptStep{{"EHLO a", 250}}); err != nil { - t.Error(err) - } - } -// Test commands in READY state func TestEmptyEnvelope(t *testing.T) { ds := test.NewStore() server := setupSMTPServer(ds) @@ -102,7 +105,7 @@ func TestEmptyEnvelope(t *testing.T) { } } -// Test AUTH +// Test AUTH commands. func TestAuth(t *testing.T) { ds := test.NewStore() server := setupSMTPServer(ds) @@ -139,15 +142,64 @@ func TestAuth(t *testing.T) { } } -// Test commands in READY state -func TestReadyState(t *testing.T) { +// Test TLS commands. +func TestTLS(t *testing.T) { ds := test.NewStore() server := setupSMTPServer(ds) defer server.Drain() - // Test out some mangled READY commands + // Test Start TLS parsing. script := []scriptStep{ {"HELO localhost", 250}, + {"STARTTLS", 454}, // TLS unconfigured. + } + + if err := playSession(t, server, script); err != nil { + t.Error(err) + } +} + +// Test valid commands in READY state. +func TestReadyStateValidCommands(t *testing.T) { + ds := test.NewStore() + server := setupSMTPServer(ds) + + // Test out some valid MAIL commands + tests := []scriptStep{ + {"MAIL FROM:", 250}, + {"MAIL FROM: ", 250}, + {"MAIL FROM: BODY=8BITMIME", 250}, + {"MAIL FROM: SIZE=1024", 250}, + {"MAIL FROM: SIZE=1024 BODY=8BITMIME", 250}, + {"MAIL FROM: SIZE=4096 AUTH=<>", 250}, + {"MAIL FROM:", 250}, + {"MAIL FROM:<\"first last\"@space.com>", 250}, + {"MAIL FROM:", 250}, + {"MAIL FROM:name@host.com>", 250}, + {"MAIL FROM:<\"user>name\"@host.com>", 250}, + {"MAIL FROM:<\"user@internal\"@external.com>", 250}, + } + + for _, tc := range tests { + t.Run(tc.send, func(t *testing.T) { + defer server.Drain() + script := []scriptStep{ + {"HELO localhost", 250}, + tc, + {"QUIT", 221}} + if err := playSession(t, server, script); err != nil { + t.Error(err) + } + }) + } +} + +// Test invalid commands in READY state. +func TestReadyStateInvalidCommands(t *testing.T) { + ds := test.NewStore() + server := setupSMTPServer(ds) + + tests := []scriptStep{ {"FOOB", 500}, {"HELO", 503}, {"DATA", 503}, @@ -159,49 +211,20 @@ func TestReadyState(t *testing.T) { {"MAIL FROM:", 501}, {"MAIL FROM:", 501}, } - if err := playSession(t, server, script); err != nil { - t.Error(err) + + for _, tc := range tests { + t.Run(tc.send, func(t *testing.T) { + defer server.Drain() + script := []scriptStep{ + {"HELO localhost", 250}, + tc, + {"QUIT", 221}} + if err := playSession(t, server, script); err != nil { + t.Error(err) + } + }) } - // Test out some valid MAIL commands - script = []scriptStep{ - {"HELO localhost", 250}, - {"MAIL FROM:", 250}, - {"RSET", 250}, - {"MAIL FROM: ", 250}, - {"RSET", 250}, - {"MAIL FROM: BODY=8BITMIME", 250}, - {"RSET", 250}, - {"MAIL FROM: SIZE=1024", 250}, - {"RSET", 250}, - {"MAIL FROM: SIZE=1024 BODY=8BITMIME", 250}, - {"RSET", 250}, - {"MAIL FROM: SIZE=4096 AUTH=<>", 250}, - {"RSET", 250}, - {"MAIL FROM:", 250}, - {"RSET", 250}, - {"MAIL FROM:<\"first last\"@space.com>", 250}, - {"RSET", 250}, - {"MAIL FROM:", 250}, - {"RSET", 250}, - {"MAIL FROM:name@host.com>", 250}, - {"RSET", 250}, - {"MAIL FROM:<\"user>name\"@host.com>", 250}, - {"RSET", 250}, - {"MAIL FROM:<\"user@internal\"@external.com>", 250}, - } - if err := playSession(t, server, script); err != nil { - t.Error(err) - } - - // Test Start TLS parsing. - script = []scriptStep{ - {"HELO localhost", 250}, - {"STARTTLS", 454}, // TLS unconfigured. - } - if err := playSession(t, server, script); err != nil { - t.Error(err) - } } // Test commands in MAIL state diff --git a/pkg/server/smtp/listener.go b/pkg/server/smtp/listener.go index 0a38282..5d1cc07 100644 --- a/pkg/server/smtp/listener.go +++ b/pkg/server/smtp/listener.go @@ -179,7 +179,6 @@ func (s *Server) serve(ctx context.Context) { func (s *Server) Drain() { // Wait for sessions to close. s.wg.Wait() - log.Debug().Str("module", "smtp").Str("phase", "shutdown").Msg("SMTP connections have drained") } // Notify allows the running SMTP server to be monitored for a fatal error.