Skip to content

Commit

Permalink
Merge pull request #158 from mocktools/develop
Browse files Browse the repository at this point in the history
Golang smtpmock v2.1.0
  • Loading branch information
bestwebua authored Jun 14, 2023
2 parents 1a98bcd + cce7367 commit d29f933
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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.1.0] - 2023-06-14

- Added ability to use `NOOP` command, following [RFC 2821](https://datatracker.ietf.org/doc/html/rfc2821#section-4.1.1.9) (section 4.1.1.9). Thanks [@rehleinBo](https://github.com/rehleinBo) for PR

## [2.0.5] - 2023-01-11

### Updated
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ smtpmock.ConfigurationAttr{
// equals to 0 seconds by default
ResponseDelayRset: 2,

// Ability to specify NOOP response delay in seconds. It runs immediately,
// equals to 0 seconds by default
ResponseDelayNoop: 2,

// Ability to specify QUIT response delay in seconds. It runs immediately,
// equals to 0 seconds by default
ResponseDelayQuit: 2,
Expand Down Expand Up @@ -262,6 +266,9 @@ smtpmock.ConfigurationAttr{
// Custom RSET received message. Based on defaultOkMsg by default
MsgRsetReceived: "msgRsetReceived",

// Custom NOOP received message. Based on defaultOkMsg by default
MsgNoopReceived: "msgNoopReceived",

// Custom quit command message. Based on defaultQuitMsg by default
MsgQuitCmd: "msgQuitCmd",
}
Expand Down Expand Up @@ -396,6 +403,7 @@ curl -sL https://raw.githubusercontent.com/mocktools/go-smtp-mock/master/script/
| `-responseDelayData` - `DATA` response delay in seconds. It's equal to 0 seconds by default | `-responseDelayData=2` |
| `-responseDelayMessage` - Message response delay in seconds. It's equal to 0 seconds by default | `-responseDelayMessage=2` |
| `-responseDelayRset` - `RSET` response delay in seconds. It's equal to 0 seconds by default | `-responseDelayRset=2` |
| `-responseDelayNoop` - `NOOP` response delay in seconds. It's equal to 0 seconds by default | `-responseDelayNoop=2` |
| `-responseDelayQuit` - `QUIT` response delay in seconds. It's equal to 0 seconds by default | `-responseDelayQuit=2` |
| `-msgSizeLimit` - message body size limit in bytes. It's equal to `10485760` bytes | `-msgSizeLimit=42` |
| `-msgGreeting` - custom server greeting message | `-msgGreeting="Greeting message"` |
Expand All @@ -420,7 +428,8 @@ curl -sL https://raw.githubusercontent.com/mocktools/go-smtp-mock/master/script/
| `-msgInvalidCmdRsetSequence` - custom invalid command `RSET` sequence message | `-msgInvalidCmdRsetSequence="Invalid command RSET sequence message"` |
| `-msgInvalidCmdRsetArg` - custom invalid command `RSET` message | `-msgInvalidCmdRsetArg="Invalid command RSET message"` |
| `-msgRsetReceived` - custom `RSET` received message | `-msgRsetReceived="RSET received message"` |
| `-msgQuitCmd` - custom quit command message | `-msgQuitCmd="Quit command message"` |
| `-msgNoopReceived` - custom `NOOP` received message | `-msgNoopReceived="NOOP received message"` |
| `-msgQuitCmd` - custom `QUIT` command message | `-msgQuitCmd="Quit command message"` |

#### Other options

Expand All @@ -444,7 +453,8 @@ Available not configuration `smtpmock` options:
| `3` | `RCPT TO` | can be used after command with id `2` and greater | `email address`, `<email address>` | `RCPT TO: user@domain.com` |
| `4` | `DATA` | can be used after command with id `3` | - | `DATA` |
| `5` | `RSET` | can be used after command with id `1` and greater | - | `RSET` |
| `6` | `QUIT` | no | - | `QUIT` |
| `6` | `NOOP` | no | - | `NOOP` |
| `7` | `QUIT` | no | - | `QUIT` |

Please note in case when same command used more the one time during same session all saved data upper this command will be erased.

Expand Down
6 changes: 5 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func attrFromCommandLine(args []string, options ...flag.ErrorHandling) (bool, *s
responseDelayData = flags.Int("responseDelayData", 0, "DATA"+responseDelayFlagInfo)
responseDelayMessage = flags.Int("responseDelayMessage", 0, "Message"+responseDelayFlagInfo)
responseDelayRset = flags.Int("responseDelayRset", 0, "RSET"+responseDelayFlagInfo)
responseDelayNoop = flags.Int("responseDelayNoop", 0, "NOOP"+responseDelayFlagInfo)
responseDelayQuit = flags.Int("responseDelayQuit", 0, "QUIT"+responseDelayFlagInfo)
msgSizeLimit = flags.Int("msgSizeLimit", 0, "Message body size limit in bytes. It's equal to 10485760 bytes")
msgGreeting = flags.String("msgGreeting", "", "Custom server greeting message")
Expand All @@ -124,7 +125,8 @@ func attrFromCommandLine(args []string, options ...flag.ErrorHandling) (bool, *s
msgInvalidCmdRsetSequence = flags.String("msgInvalidCmdRsetSequence", "", "Custom invalid command RSET sequence message")
msgInvalidCmdRsetArg = flags.String("msgInvalidCmdRsetArg", "", "Custom invalid command RSET message")
msgRsetReceived = flags.String("msgRsetReceived", "", "Custom RSET received message")
msgQuitCmd = flags.String("msgQuitCmd", "", "Custom quit command message")
msgNoopReceived = flags.String("msgNoopReceived", "", "Custom NOOP received message")
msgQuitCmd = flags.String("msgQuitCmd", "", "Custom QUIT command message")
)
if err := flags.Parse(args[1:]); err != nil {
return *ver, nil, err
Expand All @@ -150,6 +152,7 @@ func attrFromCommandLine(args []string, options ...flag.ErrorHandling) (bool, *s
ResponseDelayData: *responseDelayData,
ResponseDelayMessage: *responseDelayMessage,
ResponseDelayRset: *responseDelayRset,
ResponseDelayNoop: *responseDelayNoop,
ResponseDelayQuit: *responseDelayQuit,
MsgSizeLimit: *msgSizeLimit,
MsgGreeting: *msgGreeting,
Expand All @@ -174,6 +177,7 @@ func attrFromCommandLine(args []string, options ...flag.ErrorHandling) (bool, *s
MsgInvalidCmdRsetSequence: *msgInvalidCmdRsetSequence,
MsgInvalidCmdRsetArg: *msgInvalidCmdRsetArg,
MsgRsetReceived: *msgRsetReceived,
MsgNoopReceived: *msgNoopReceived,
MsgQuitCmd: *msgQuitCmd,
}, nil
}
8 changes: 7 additions & 1 deletion cmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ func TestAttrFromCommandLine(t *testing.T) {
responseDelayData := 4
responseDelayMessage := 5
responseDelayRset := 6
responseDelayQuit := 7
responseDelayNoop := 7
responseDelayQuit := 8
msgSizeLimit := 1000
msgGreeting := "msgGreeting"
msgInvalidCmd := "msgInvalidCmd"
Expand All @@ -122,6 +123,7 @@ func TestAttrFromCommandLine(t *testing.T) {
msgMsgSizeIsTooBig := "msgMsgSizeIsTooBig"
msgMsgReceived := "msgMsgReceived"
msgRsetReceived := "msgRsetReceived"
msgNoopReceived := "msgNoopReceived"
msgQuitCmd := "msgQuitCmd"
ver, configAttr, err := attrFromCommandLine(
[]string{
Expand All @@ -145,6 +147,7 @@ func TestAttrFromCommandLine(t *testing.T) {
"-responseDelayData=" + strconv.Itoa(responseDelayData),
"-responseDelayMessage=" + strconv.Itoa(responseDelayMessage),
"-responseDelayRset=" + strconv.Itoa(responseDelayRset),
"-responseDelayNoop=" + strconv.Itoa(responseDelayNoop),
"-responseDelayQuit=" + strconv.Itoa(responseDelayQuit),
"-msgSizeLimit=" + strconv.Itoa(msgSizeLimit),
"-msgGreeting=" + msgGreeting,
Expand All @@ -167,6 +170,7 @@ func TestAttrFromCommandLine(t *testing.T) {
"-msgMsgSizeIsTooBig=" + msgMsgSizeIsTooBig,
"-msgMsgReceived=" + msgMsgReceived,
"-msgRsetReceived=" + msgRsetReceived,
"-msgNoopReceived=" + msgNoopReceived,
"-msgQuitCmd=" + msgQuitCmd,
},
)
Expand All @@ -191,6 +195,7 @@ func TestAttrFromCommandLine(t *testing.T) {
assert.Equal(t, responseDelayData, configAttr.ResponseDelayData)
assert.Equal(t, responseDelayMessage, configAttr.ResponseDelayMessage)
assert.Equal(t, responseDelayRset, configAttr.ResponseDelayRset)
assert.Equal(t, responseDelayNoop, configAttr.ResponseDelayNoop)
assert.Equal(t, responseDelayQuit, configAttr.ResponseDelayQuit)
assert.Equal(t, msgSizeLimit, configAttr.MsgSizeLimit)
assert.Equal(t, msgGreeting, configAttr.MsgGreeting)
Expand All @@ -213,6 +218,7 @@ func TestAttrFromCommandLine(t *testing.T) {
assert.Equal(t, msgMsgSizeIsTooBig, configAttr.MsgMsgSizeIsTooBig)
assert.Equal(t, msgMsgReceived, configAttr.MsgMsgReceived)
assert.Equal(t, msgRsetReceived, configAttr.MsgRsetReceived)
assert.Equal(t, msgNoopReceived, configAttr.MsgNoopReceived)
assert.Equal(t, msgQuitCmd, configAttr.MsgQuitCmd)
assert.NoError(t, err)
})
Expand Down
14 changes: 14 additions & 0 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type configuration struct {
msgInvalidCmdRsetSequence string
msgInvalidCmdRsetArg string
msgRsetReceived string
msgNoopReceived string
blacklistedHeloDomains []string
blacklistedMailfromEmails []string
blacklistedRcpttoEmails []string
Expand All @@ -44,6 +45,7 @@ type configuration struct {
responseDelayData int
responseDelayMessage int
responseDelayRset int
responseDelayNoop int
responseDelayQuit int
msgSizeLimit int
sessionTimeout int
Expand Down Expand Up @@ -86,6 +88,7 @@ func newConfiguration(config ConfigurationAttr) *configuration {
msgInvalidCmdRsetSequence: config.MsgInvalidCmdRsetSequence,
msgInvalidCmdRsetArg: config.MsgInvalidCmdRsetArg,
msgRsetReceived: config.MsgRsetReceived,
msgNoopReceived: config.MsgNoopReceived,
msgQuitCmd: config.MsgQuitCmd,
blacklistedHeloDomains: config.BlacklistedHeloDomains,
blacklistedMailfromEmails: config.BlacklistedMailfromEmails,
Expand All @@ -97,6 +100,7 @@ func newConfiguration(config ConfigurationAttr) *configuration {
responseDelayData: config.ResponseDelayData,
responseDelayMessage: config.ResponseDelayMessage,
responseDelayRset: config.ResponseDelayRset,
responseDelayNoop: config.ResponseDelayNoop,
responseDelayQuit: config.ResponseDelayQuit,
msgSizeLimit: config.MsgSizeLimit,
sessionTimeout: config.SessionTimeout,
Expand Down Expand Up @@ -136,6 +140,7 @@ type ConfigurationAttr struct {
MsgInvalidCmdRsetSequence string
MsgInvalidCmdRsetArg string
MsgRsetReceived string
MsgNoopReceived string
BlacklistedHeloDomains []string
BlacklistedMailfromEmails []string
BlacklistedRcpttoEmails []string
Expand All @@ -146,6 +151,7 @@ type ConfigurationAttr struct {
ResponseDelayData int
ResponseDelayMessage int
ResponseDelayRset int
ResponseDelayNoop int
ResponseDelayQuit int
MsgSizeLimit int
SessionTimeout int
Expand Down Expand Up @@ -263,6 +269,13 @@ func (config *ConfigurationAttr) assignHandlerRsetDefaultValues() {
}
}

// Assigns handlerRset defaults
func (config *ConfigurationAttr) assignHandlerNoopDefaultValues() {
if config.MsgNoopReceived == emptyString {
config.MsgNoopReceived = defaultOkMsg
}
}

// Assigns default values to ConfigurationAttr fields
func (config *ConfigurationAttr) assignDefaultValues() {
config.assignServerDefaultValues()
Expand All @@ -272,4 +285,5 @@ func (config *ConfigurationAttr) assignDefaultValues() {
config.assignHandlerDataDefaultValues()
config.assignHandlerMessageDefaultValues()
config.assignHandlerRsetDefaultValues()
config.assignHandlerNoopDefaultValues()
}
10 changes: 10 additions & 0 deletions configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ func TestNewConfiguration(t *testing.T) {
assert.Equal(t, defaultInvalidCmdMsg, buildedConfiguration.msgInvalidCmdRsetArg)
assert.Equal(t, defaultOkMsg, buildedConfiguration.msgRsetReceived)

assert.Equal(t, defaultOkMsg, buildedConfiguration.msgNoopReceived)

assert.Equal(t, fmt.Sprintf(defaultMsgSizeIsTooBigMsg+" %d bytes", defaultMessageSizeLimit), buildedConfiguration.msgMsgSizeIsTooBig)
assert.Equal(t, defaultReceivedMsg, buildedConfiguration.msgMsgReceived)
assert.Equal(t, defaultMessageSizeLimit, buildedConfiguration.msgSizeLimit)
Expand All @@ -61,6 +63,7 @@ func TestNewConfiguration(t *testing.T) {
assert.Equal(t, defaultSessionResponseDelay, buildedConfiguration.responseDelayData)
assert.Equal(t, defaultSessionResponseDelay, buildedConfiguration.responseDelayMessage)
assert.Equal(t, defaultSessionResponseDelay, buildedConfiguration.responseDelayRset)
assert.Equal(t, defaultSessionResponseDelay, buildedConfiguration.responseDelayNoop)
assert.Equal(t, defaultSessionResponseDelay, buildedConfiguration.responseDelayQuit)
})

Expand Down Expand Up @@ -96,6 +99,7 @@ func TestNewConfiguration(t *testing.T) {
MsgInvalidCmdRsetSequence: "msgInvalidCmdRsetSequence",
MsgInvalidCmdRsetArg: "msgInvalidCmdRsetArg",
MsgRsetReceived: "msgRsetReceived",
MsgNoopReceived: "msgNoopReceived",
BlacklistedHeloDomains: []string{},
BlacklistedMailfromEmails: []string{},
NotRegisteredEmails: []string{},
Expand All @@ -106,6 +110,7 @@ func TestNewConfiguration(t *testing.T) {
ResponseDelayData: 2,
ResponseDelayMessage: 2,
ResponseDelayRset: 2,
ResponseDelayNoop: 2,
ResponseDelayQuit: 2,
MsgSizeLimit: 42,
SessionTimeout: 120,
Expand Down Expand Up @@ -149,6 +154,8 @@ func TestNewConfiguration(t *testing.T) {
assert.Equal(t, configAttr.MsgInvalidCmdRsetArg, buildedConfiguration.msgInvalidCmdRsetArg)
assert.Equal(t, configAttr.MsgRsetReceived, buildedConfiguration.msgRsetReceived)

assert.Equal(t, configAttr.MsgNoopReceived, buildedConfiguration.msgNoopReceived)

assert.Equal(t, fmt.Sprintf(defaultMsgSizeIsTooBigMsg+" %d bytes", configAttr.MsgSizeLimit), buildedConfiguration.msgMsgSizeIsTooBig)
assert.Equal(t, configAttr.MsgMsgReceived, buildedConfiguration.msgMsgReceived)
assert.Equal(t, configAttr.MsgSizeLimit, buildedConfiguration.msgSizeLimit)
Expand All @@ -164,6 +171,7 @@ func TestNewConfiguration(t *testing.T) {
assert.Equal(t, configAttr.ResponseDelayData, buildedConfiguration.responseDelayData)
assert.Equal(t, configAttr.ResponseDelayMessage, buildedConfiguration.responseDelayMessage)
assert.Equal(t, configAttr.ResponseDelayRset, buildedConfiguration.responseDelayRset)
assert.Equal(t, configAttr.ResponseDelayNoop, buildedConfiguration.responseDelayNoop)
assert.Equal(t, configAttr.ResponseDelayQuit, buildedConfiguration.responseDelayQuit)
})
}
Expand Down Expand Up @@ -203,6 +211,8 @@ func TestConfigurationAttrAssignDefaultValues(t *testing.T) {
assert.Equal(t, defaultInvalidCmdMsg, configurationAttr.MsgInvalidCmdRsetArg)
assert.Equal(t, defaultOkMsg, configurationAttr.MsgRsetReceived)

assert.Equal(t, defaultOkMsg, configurationAttr.MsgNoopReceived)

assert.Equal(t, fmt.Sprintf(defaultMsgSizeIsTooBigMsg+" %d bytes", defaultMessageSizeLimit), configurationAttr.MsgMsgSizeIsTooBig)
assert.Equal(t, defaultReceivedMsg, configurationAttr.MsgMsgReceived)
assert.Equal(t, defaultMessageSizeLimit, configurationAttr.MsgSizeLimit)
Expand Down
5 changes: 3 additions & 2 deletions consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const (
defaultInvalidCmdHeloArgMsg = "501 HELO requires domain address or valid address literal"
defaultInvalidCmdMailfromArgMsg = "501 MAIL FROM requires valid email address"
defaultInvalidCmdRcpttoArgMsg = "501 RCPT TO requires valid email address"
defaultInvalidCmdMsg = "502 Command unrecognized. Available commands: HELO, EHLO, MAIL FROM:, RCPT TO:, DATA, RSET, QUIT"
defaultInvalidCmdMsg = "502 Command unrecognized. Available commands: HELO, EHLO, MAIL FROM:, RCPT TO:, DATA, RSET, NOOP, QUIT"
defaultInvalidCmdHeloSequenceMsg = "503 Bad sequence of commands. HELO should be the first"
defaultInvalidCmdMailfromSequenceMsg = "503 Bad sequence of commands. MAIL FROM should be used after HELO"
defaultInvalidCmdRcpttoSequenceMsg = "503 Bad sequence of commands. RCPT TO should be used after MAIL FROM"
Expand Down Expand Up @@ -51,7 +51,7 @@ const (
serverForceStopMsg = "SMTP mock server was force stopped by timeout"

// Regex patterns
availableCmdsRegexPattern = `(?i)helo|ehlo|mail from:|rcpt to:|data|rset|quit`
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})`
emailRegexPattern = `(?i)<?((.+)@` + domainRegexPattern + `)>?`
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}`
Expand All @@ -62,6 +62,7 @@ const (
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 + `|localhost|` + ipAddressRegexPattern + addressLiteralRegexPattern + `)\z`
validMailromComplexCmdRegexPattern = `\A(` + validMailfromCmdRegexPattern + `) ?(` + emailRegexPattern + `)\z`
Expand Down
29 changes: 29 additions & 0 deletions handler_noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package smtpmock

// NOOP command handler
type handlerNoop struct {
*handler
}

// NOOP command handler builder. Returns pointer to new handlerNoop structure
func newHandlerNoop(session sessionInterface, message *Message, configuration *configuration) *handlerNoop {
return &handlerNoop{&handler{session: session, message: message, configuration: configuration}}
}

// NOOP handler methods

// Main NOOP handler runner
func (handler *handlerNoop) run(request string) {
if handler.isInvalidRequest(request) {
return
}

handler.message.noop = true
configuration := handler.configuration
handler.session.writeResponse(configuration.msgNoopReceived, configuration.responseDelayNoop)
}

// Invalid NOOP command predicate. Returns true when request is invalid, otherwise returns false
func (handler *handlerNoop) isInvalidRequest(request string) bool {
return !matchRegex(request, validNoopCmdRegexPattern)
}
54 changes: 54 additions & 0 deletions handler_noop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package smtpmock

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewHandlerNoop(t *testing.T) {
t.Run("returns new handleNoop", func(t *testing.T) {
session, message, configuration := new(session), new(Message), new(configuration)
handler := newHandlerNoop(session, message, configuration)

assert.Same(t, session, handler.session)
assert.Same(t, message, handler.message)
assert.Same(t, configuration, handler.configuration)
})
}

func TestHandlerNoopRun(t *testing.T) {
t.Run("when successful NOOP request", func(t *testing.T) {
request, session, message, configuration := "NOOP", new(sessionMock), new(Message), createConfiguration()
receivedMessage := configuration.msgNoopReceived
handler := newHandlerNoop(session, message, configuration)
session.On("writeResponse", receivedMessage, configuration.responseDelayQuit).Once().Return(nil)
handler.run(request)

assert.True(t, message.noop)
})

t.Run("when failure NOOP request", func(t *testing.T) {
request, session, message, configuration := "NOOP ", new(sessionMock), new(Message), createConfiguration()
handler := newHandlerNoop(session, message, configuration)
handler.run(request)

assert.False(t, message.noop)
})
}

func TestHandlerNoopIsInvalidRequest(t *testing.T) {
handler := newHandlerNoop(new(session), new(Message), new(configuration))

t.Run("when request includes invalid NOOP command", func(t *testing.T) {
request := "NOOP "

assert.True(t, handler.isInvalidRequest(request))
})

t.Run("when request includes valid NOOP command", func(t *testing.T) {
request := "NOOP"

assert.False(t, handler.isInvalidRequest(request))
})
}
Loading

0 comments on commit d29f933

Please sign in to comment.