diff --git a/.circleci/.goreleaser.yml b/.circleci/.goreleaser.yml index 7f53071..6fc939e 100644 --- a/.circleci/.goreleaser.yml +++ b/.circleci/.goreleaser.yml @@ -4,7 +4,7 @@ version: 2 builds: - id: "smtpmock-build" - dir: cmd + dir: cmd/smtpmock binary: smtpmock env: - CGO_ENABLED=0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a29273..2a523fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.3] - 2024-11-17 + +### Fixed + +- Fixed issue with [invalid name with email address parsing](https://github.com/mocktools/go-smtp-mock/issues/153) for `MAIL FROM` and `RCPT TO` commands +- Fixed flaky tests + +### Updated + +- Updated `cmd` namespace +- Updated `goreleaser` config + ## [2.3.2] - 2024-11-14 ### Fixed diff --git a/cmd/main.go b/cmd/smtpmock/main.go similarity index 100% rename from cmd/main.go rename to cmd/smtpmock/main.go diff --git a/cmd/main_test.go b/cmd/smtpmock/main_test.go similarity index 100% rename from cmd/main_test.go rename to cmd/smtpmock/main_test.go diff --git a/cmd/test_mocks_test.go b/cmd/smtpmock/test_mocks_test.go similarity index 100% rename from cmd/test_mocks_test.go rename to cmd/smtpmock/test_mocks_test.go diff --git a/consts.go b/consts.go index 5a6637b..fe613d6 100644 --- a/consts.go +++ b/consts.go @@ -53,20 +53,20 @@ const ( // Regex patterns availableCmdsRegexPattern = `(?i)helo|ehlo|mail from:|rcpt to:|data|rset|noop|quit` domainRegexPattern = `(?i)([\p{L}0-9]+([\-.]{1}[\p{L}0-9]+)*\.\p{L}{2,63}|localhost)` - emailRegexPattern = `(?i)?` + emailRegexPattern = `(?i)(?:[\p{L}\p{N}\s]*?*` ipAddressRegexPattern = `(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}` addressLiteralRegexPattern = `|\[` + ipAddressRegexPattern + `\]` - validHeloCmdsRegexPattern = `(?i)helo|ehlo` - validMailfromCmdRegexPattern = `(?i)mail from:` - validRcpttoCmdRegexPattern = `(?i)rcpt to:` - validDataCmdRegexPattern = `\A(?i)data\z` - validRsetCmdRegexPattern = `\A(?i)rset\z` - validNoopCmdRegexPattern = `\A(?i)noop\z` - validQuitCmdRegexPattern = `\A(?i)quit\z` - validHeloComplexCmdRegexPattern = `\A(` + validHeloCmdsRegexPattern + `) (` + domainRegexPattern + `|` + ipAddressRegexPattern + addressLiteralRegexPattern + `)\z` - validMailromComplexCmdRegexPattern = `\A(` + validMailfromCmdRegexPattern + `) ?(` + emailRegexPattern + `)\z` - validRcpttoComplexCmdRegexPattern = `\A(` + validRcpttoCmdRegexPattern + `) ?(` + emailRegexPattern + `)\z` + validHeloCmdsRegexPattern = `(?i)helo|ehlo` + validMailfromCmdRegexPattern = `(?i)mail from:` + validRcpttoCmdRegexPattern = `(?i)rcpt to:` + validDataCmdRegexPattern = `\A(?i)data\z` + validRsetCmdRegexPattern = `\A(?i)rset\z` + validNoopCmdRegexPattern = `\A(?i)noop\z` + validQuitCmdRegexPattern = `\A(?i)quit\z` + validHeloComplexCmdRegexPattern = `\A(` + validHeloCmdsRegexPattern + `) (` + domainRegexPattern + `|` + ipAddressRegexPattern + addressLiteralRegexPattern + `)\z` + validMailfromComplexCmdRegexPattern = `\A(` + validMailfromCmdRegexPattern + `)\s*` + emailRegexPattern + `\z` + validRcpttoComplexCmdRegexPattern = `\A(` + validRcpttoCmdRegexPattern + `)\s*` + emailRegexPattern + `\z` // Helpers emptyString = "" diff --git a/handler_mailfrom.go b/handler_mailfrom.go index 26e50a2..c49e5d1 100644 --- a/handler_mailfrom.go +++ b/handler_mailfrom.go @@ -62,7 +62,7 @@ func (handler *handlerMailfrom) isInvalidCmdSequence(request string) bool { // Invalid MAILFROM command argument predicate. Returns true and writes result for case when // MAILFROM command argument is invalid, otherwise returns false func (handler *handlerMailfrom) isInvalidCmdArg(request string) bool { - if !matchRegex(request, validMailromComplexCmdRegexPattern) { + if !matchRegex(request, validMailfromComplexCmdRegexPattern) { return handler.writeResult(false, request, handler.configuration.msgInvalidCmdMailfromArg) } @@ -71,7 +71,7 @@ func (handler *handlerMailfrom) isInvalidCmdArg(request string) bool { // Returns email from MAILFROM request func (handler *handlerMailfrom) mailfromEmail(request string) string { - return regexCaptureGroup(request, validMailromComplexCmdRegexPattern, 3) + return regexCaptureGroup(request, validMailfromComplexCmdRegexPattern, 2) } // Custom behavior for MAILFROM email. Returns true and writes result for case when diff --git a/handler_mailfrom_test.go b/handler_mailfrom_test.go index 0393c0b..3c47655 100644 --- a/handler_mailfrom_test.go +++ b/handler_mailfrom_test.go @@ -226,20 +226,28 @@ func TestHandlerMaifromIsInvalidCmdArg(t *testing.T) { } func TestHandlerMailfromMailfromEmail(t *testing.T) { - handler := new(handlerMailfrom) + validEmail, handler := "user@example.com", new(handlerMailfrom) t.Run("when request includes valid email address without <> sign", func(t *testing.T) { - validEmail := "user@example.com" - assert.Equal(t, validEmail, handler.mailfromEmail("MAIL FROM: "+validEmail)) }) t.Run("when request includes valid email address with <> sign", func(t *testing.T) { - validEmail := "user@example.com" - assert.Equal(t, validEmail, handler.mailfromEmail("MAIL FROM: "+"<"+validEmail+">")) }) + t.Run("when request includes valid email address without <> sign, with name", func(t *testing.T) { + assert.Equal(t, validEmail, handler.mailfromEmail("MAIL FROM: John Doe <"+validEmail+">")) + }) + + t.Run("when request includes valid email address with <> sign, name with space", func(t *testing.T) { + assert.Equal(t, validEmail, handler.mailfromEmail("MAIL FROM: "+">")) + }) + + t.Run("when request includes valid email address with <> sign and name without space", func(t *testing.T) { + assert.Equal(t, validEmail, handler.mailfromEmail("MAIL FROM: "+">")) + }) + t.Run("when request includes invalid email address", func(t *testing.T) { invalidEmail := "user@invalid" diff --git a/handler_rcptto.go b/handler_rcptto.go index 373a128..5db7396 100644 --- a/handler_rcptto.go +++ b/handler_rcptto.go @@ -88,7 +88,7 @@ func (handler *handlerRcptto) isInvalidCmdArg(request string) bool { // Returns email from RCPTTO request func (handler *handlerRcptto) rcpttoEmail(request string) string { - return regexCaptureGroup(request, validRcpttoComplexCmdRegexPattern, 3) + return regexCaptureGroup(request, validRcpttoComplexCmdRegexPattern, 2) } // Custom behavior for RCPTTO email. Returns true and writes result for case when diff --git a/handler_rcptto_test.go b/handler_rcptto_test.go index c98247e..242a45e 100644 --- a/handler_rcptto_test.go +++ b/handler_rcptto_test.go @@ -326,20 +326,28 @@ func TestHandlerRcpttoIsInvalidCmdArg(t *testing.T) { } func TestHandlerRcpttoRcpttoEmail(t *testing.T) { - handler := new(handlerRcptto) + validEmail, handler := "user@example.com", new(handlerRcptto) t.Run("when request includes valid email address without <> sign", func(t *testing.T) { - validEmail := "user@example.com" - assert.Equal(t, validEmail, handler.rcpttoEmail("RCPT TO: "+validEmail)) }) t.Run("when request includes valid email address with <> sign", func(t *testing.T) { - validEmail := "user@example.com" - assert.Equal(t, validEmail, handler.rcpttoEmail("RCPT TO: "+"<"+validEmail+">")) }) + t.Run("when request includes valid email address without <> sign, with name", func(t *testing.T) { + assert.Equal(t, validEmail, handler.rcpttoEmail("RCPT TO: John Doe <"+validEmail+">")) + }) + + t.Run("when request includes valid email address with <> sign, name with space", func(t *testing.T) { + assert.Equal(t, validEmail, handler.rcpttoEmail("RCPT TO: "+">")) + }) + + t.Run("when request includes valid email address with <> sign and name without space", func(t *testing.T) { + assert.Equal(t, validEmail, handler.rcpttoEmail("RCPT TO: "+">")) + }) + t.Run("when request includes invalid email address", func(t *testing.T) { invalidEmail := "user@invalid" diff --git a/helpers.go b/helpers.go index 7df232d..6d07683 100644 --- a/helpers.go +++ b/helpers.go @@ -3,6 +3,7 @@ package smtpmock import ( "fmt" "regexp" + "time" ) // Regex builder @@ -20,8 +21,8 @@ func matchRegex(strContext, regexPattern string) bool { return regex.MatchString(strContext) } -// Returns string by regex pattern capture group index. For cases when regex not matched or -// capture group not found returns empty string +// Returns string by regex pattern capture group index. +// For cases when regex not matched or capture group not found returns empty string func regexCaptureGroup(str string, regexPattern string, captureGroup int) (capturedString string) { var regex *regexp.Regexp defer func() { _ = recover() }() @@ -47,3 +48,8 @@ func isIncluded(slice []string, target string) bool { func serverWithPortNumber(server string, portNumber int) string { return fmt.Sprintf("%s:%d", server, portNumber) } + +// Sleeps for the given duration in milliseconds +func sleepMilliseconds(duration int) { + time.Sleep(time.Duration(duration) * time.Millisecond) +} diff --git a/smtpmock_test.go b/smtpmock_test.go index 07d4271..83d0eb4 100644 --- a/smtpmock_test.go +++ b/smtpmock_test.go @@ -3,7 +3,6 @@ package smtpmock import ( "fmt" "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -154,9 +153,12 @@ func TestNew(t *testing.T) { assert.False(t, server.isStarted()) assert.NoError(t, server.Start()) + sleepMilliseconds(100) assert.True(t, server.isStarted()) _ = runSuccessfulSMTPSession(configuration.hostAddress, server.PortNumber(), true, 0) + sleepMilliseconds(100) _ = server.Stop() + sleepMilliseconds(100) assert.Equal(t, 2, len(server.Messages())) assert.NotNil(t, server.quit) @@ -221,11 +223,11 @@ func TestServerMessagesRaceCondition(t *testing.T) { }() // ensure that server.MessagesAndPurge() doesn't touch messages from active SMTP-session - time.Sleep(5 * time.Millisecond) + sleepMilliseconds(5) assert.Empty(t, server.MessagesAndPurge()) // ensure that messages appears after SMTP-session - time.Sleep(100 * time.Millisecond) + sleepMilliseconds(100) assert.Len(t, server.Messages(), 1) if err := server.Stop(); err != nil {