GithubHelp home page GithubHelp logo

mocktools / go-smtp-mock Goto Github PK

View Code? Open in Web Editor NEW
116.0 5.0 17.0 276 KB

SMTP mock server written on Golang. Mimic any ๐Ÿ“ค SMTP server behavior for your test environment with fake SMTP server.

License: MIT License

Go 98.89% Shell 1.11%
smtp smtp-mock smtp-server smtp-mail smtplib mock-server go golang go-smtp mocktools fake-server fake-smtp-server hacktoberfest testing testing-tools developer-tools

go-smtp-mock's Introduction

SMTP mock server written on Golang. Mimic any SMTP server behavior for your test environment with fake SMTP server

Go Report Card Codecov CircleCI GitHub release (latest by date) PkgGoDev Mentioned in Awesome Go GitHub Contributor Covenant

smtpmock is lightweight configurable multithreaded fake SMTP server written in Go. It meets the minimum requirements specified by RFC 2821 & RFC 5321. Allows to mimic any SMTP server behavior for your test environment and even more ๐Ÿš€

Table of Contents

Features

  • Configurable multithreaded RFC compatible SMTP server
  • Implements the minimum command set, responds to commands and adds a valid received header to messages as specified in RFC 2821 & RFC 5321
  • Ability to configure behavior for each SMTP command
  • Comes with default settings out of the box, configure only what you need
  • Ability to override previous SMTP commands
  • Fail fast scenario (ability to close client session for case when command was inconsistent or failed)
  • Multiple receivers (ability to configure multiple RCPT TO commands receiving during one session)
  • Multiple message receiving (ability to configure multiple message receiving during one session)
  • Mock-server activity logger
  • Ability to do graceful/force shutdown of SMTP mock server
  • No authentication support
  • Zero runtime dependencies
  • Ability to access to server messages
  • Simple and intuitive DSL
  • Ability to run server as binary with command line arguments

Requirements

Golang 1.15+

Installation

Install smtpmock:

go get github.com/mocktools/go-smtp-mock/v2
go install github.com/mocktools/go-smtp-mock/v2

Import smtpmock dependency into your code:

package main

import smtpmock "github.com/mocktools/go-smtp-mock/v2"

Usage

Inside of Golang ecosystem

You have to create your SMTP mock server using smtpmock.New() and smtpmock.ConfigurationAttr{} to start interaction with it.

Configuring

smtpmock is SMTP server for test environment with configurable behavior. It comes with default settings out of the box. But you can override any default behavior if you need.

smtpmock.ConfigurationAttr{

  // Customizing server behavior
  // ---------------------------------------------------------------------
  // Host address where smtpmock will run, it's equal to "127.0.0.1" by default
  HostAddress:                   "[::]",

  // Port number on which the server will bind. If it not specified, it will be
  // assigned dynamically after server.Start() by default
  PortNumber:                    2525,

  // Enables/disables log to stdout. It's equal to false by default
  LogToStdout:                   true,

  // Enables/disables log server activity. It's equal to false by default
  LogServerActivity:             true,

  // Ability to specify session timeout. It's equal to 30 seconds by default
  SessionTimeout:                42,

  // Ability to specify graceful shutdown timeout. It's equal to 1 second by default
  ShutdownTimeout:               5,


  // Customizing SMTP command handlers behavior
  // ---------------------------------------------------------------------
  // Ability to configure fail fast scenario. It means that server will
  // close client session for case when command was inconsistent or failed.
  // It's equal to false by default
  IsCmdFailFast:                 true,

  // Ability to configure multiple RCPT TO command receiving during one session.
  // It means that server will handle and save all RCPT TO command request-response
  // pairs until receive successful response and next SMTP command has been passed.
  // Please note, by default will be saved only one, the last RCPT TO command
  // request-response pair. It's equal to false by default
  MultipleRcptto:                true,

  // Ability to configure multiple message receiving during one session. It means that server
  // will create another message during current SMTP session in case when RSET
  // command has been used after successful commands chain: MAIL FROM, RCPT TO, DATA.
  // Please note, by default RSET command flushes current message in any case.
  // It's equal to false by default
  MultipleMessageReceiving:      true,

  // Ability to specify blacklisted HELO domains. It's equal to empty []string
  BlacklistedHeloDomains:        []string{"example1.com", "example2.com", "localhost"},

  // Ability to specify blacklisted MAIL FROM emails. It's equal to empty []string
  BlacklistedMailfromEmails:     []string{"[email protected]", "[email protected]"},

  // Ability to specify blacklisted RCPT TO emails. It's equal to empty []string
  BlacklistedRcpttoEmails:       []string{"[email protected]", "[email protected]"},

  // Ability to specify not registered (non-existent) RCPT TO emails.
  // It's equal to empty []string
  NotRegisteredEmails:           []string{"[email protected]", "[email protected]"},

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

  // Ability to specify MAIL FROM response delay in seconds. It runs immediately,
  // equals to 0 seconds by default
  ResponseDelayMailfrom:         2,

  // Ability to specify RCPT TO response delay in seconds. It runs immediately,
  // equals to 0 seconds by default
  ResponseDelayRcptto:           2,

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

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

  // Ability to specify RSET response delay in seconds. It runs immediately,
  // 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,

  // Ability to specify message body size limit. It's equal to 10485760 bytes (10MB) by default
  MsgSizeLimit:                  5,


  // Customizing SMTP command handler messages context
  // ---------------------------------------------------------------------
  // Custom server greeting message. Base on defaultGreetingMsg by default
  MsgGreeting:                   "msgGreeting",

  // Custom invalid command message. Based on defaultInvalidCmdMsg by default
  MsgInvalidCmd:                 "msgInvalidCmd",

  // Custom invalid command HELO sequence message.
  // Based on defaultInvalidCmdHeloSequenceMsg by default
  MsgInvalidCmdHeloSequence:     "msgInvalidCmdHeloSequence",

  // Custom invalid command HELO argument message.
  // Based on defaultInvalidCmdHeloArgMsg by default
  MsgInvalidCmdHeloArg:          "msgInvalidCmdHeloArg",

  // Custom HELO blacklisted domain message. Based on defaultQuitMsg by default
  MsgHeloBlacklistedDomain:      "msgHeloBlacklistedDomain",

  // Custom HELO received message. Based on defaultReceivedMsg by default
  MsgHeloReceived:               "msgHeloReceived",

  // Custom invalid command MAIL FROM sequence message.
  // Based on defaultInvalidCmdMailfromSequenceMsg by default
  MsgInvalidCmdMailfromSequence: "msgInvalidCmdMailfromSequence",

  // Custom invalid command MAIL FROM argument message.
  // Based on defaultInvalidCmdMailfromArgMsg by default
  MsgInvalidCmdMailfromArg:      "msgInvalidCmdMailfromArg",

  // Custom MAIL FROM blacklisted email message. Based on defaultQuitMsg by default
  MsgMailfromBlacklistedEmail:   "msgMailfromBlacklistedEmail",

  // Custom MAIL FROM received message. Based on defaultReceivedMsg by default
  MsgMailfromReceived:           "msgMailfromReceived",

  // Custom invalid command RCPT TO sequence message.
  // Based on defaultInvalidCmdRcpttoSequenceMsg by default
  MsgInvalidCmdRcpttoSequence:   "msgInvalidCmdRcpttoSequence",

  // Custom invalid command RCPT TO argument message.
  // Based on defaultInvalidCmdRcpttoArgMsg by default
  MsgInvalidCmdRcpttoArg:        "msgInvalidCmdRcpttoArg",

  // Custom RCPT TO not registered email message.
  // Based on defaultNotRegisteredRcpttoEmailMsg by default
  MsgRcpttoNotRegisteredEmail:   "msgRcpttoNotRegisteredEmail",

  // Custom RCPT TO blacklisted email message. Based on defaultQuitMsg by default
  MsgRcpttoBlacklistedEmail:     "msgRcpttoBlacklistedEmail",

  // Custom RCPT TO received message. Based on defaultReceivedMsg by default
  MsgRcpttoReceived:             "msgRcpttoReceived",

  // Custom invalid command DATA sequence message.
  // Based on defaultInvalidCmdDataSequenceMsg by default
  MsgInvalidCmdDataSequence:     "msgInvalidCmdDataSequence",

  // Custom DATA received message. Based on defaultReadyForReceiveMsg by default
  MsgDataReceived:               "msgDataReceived",

  // Custom size is too big message. Based on defaultMsgSizeIsTooBigMsg by default
  MsgMsgSizeIsTooBig:            "msgMsgSizeIsTooBig",

  // Custom received message body message. Based on defaultReceivedMsg by default
  MsgMsgReceived:                "msgMsgReceived",

  // Custom invalid command RSET sequence message.
  // Based on defaultInvalidCmdHeloSequenceMsg by default
  MsgInvalidCmdRsetSequence:     "msgInvalidCmdRsetSequence",

  // Custom invalid command RSET message. Based on defaultInvalidCmdMsg by default
  MsgInvalidCmdRsetArg:           "msgInvalidCmdRsetArg",

  // 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",
}

Manipulation with server

package main

import (
  "fmt"
  "net"
  "net/smtp"

  smtpmock "github.com/mocktools/go-smtp-mock/v2"
)

func main() {
  // You can pass empty smtpmock.ConfigurationAttr{}. It means that smtpmock will use default settings
  server := smtpmock.New(smtpmock.ConfigurationAttr{
    LogToStdout:       true,
    LogServerActivity: true,
  })

  // To start server use Start() method
  if err := server.Start(); err != nil {
    fmt.Println(err)
  }

  // Server's port will be assigned dynamically after server.Start()
  // for case when portNumber wasn't specified
  hostAddress, portNumber := "127.0.0.1", server.PortNumber()

  // Possible SMTP-client stuff for iteration with mock server
  address := fmt.Sprintf("%s:%d", hostAddress, portNumber)
  timeout := time.Duration(2) * time.Second

  connection, _ := net.DialTimeout("tcp", address, timeout)
  client, _ := smtp.NewClient(connection, hostAddress)
  client.Hello("example.com")
  client.Quit()
  client.Close()

  // Each result of SMTP session will be saved as message.
  // To get access for server messages copies use Messages() method
  server.Messages()

  // To get access for server messages copies and purge it on server after
  // use MessagesAndPurge() method
  server.MessagesAndPurge()

  // To stop the server use Stop() method. Please note, smtpmock uses graceful shutdown.
  // It means that smtpmock will end all sessions after client responses or by session
  // timeouts immediately.
  if err := server.Stop(); err != nil {
    fmt.Println(err)
  }
}

Code from example above will produce next output to stdout:

INFO: 2021/11/30 22:07:30.554827 SMTP mock server started on port: 2525
INFO: 2021/11/30 22:07:30.554961 SMTP session started
INFO: 2021/11/30 22:07:30.554998 SMTP response: 220 Welcome
INFO: 2021/11/30 22:07:30.555059 SMTP request: EHLO example.com
INFO: 2021/11/30 22:07:30.555648 SMTP response: 250 Received
INFO: 2021/11/30 22:07:30.555686 SMTP request: QUIT
INFO: 2021/11/30 22:07:30.555722 SMTP response: 221 Closing connection
INFO: 2021/11/30 22:07:30.555732 SMTP session finished
WARNING: 2021/11/30 22:07:30.555801 SMTP mock server is in the shutdown mode and won't accept new connections
INFO: 2021/11/30 22:07:30.555808 SMTP mock server was stopped successfully

Inside of Ruby ecosystem

In Ruby ecosystem smtpmock is available as smtp_mock gem. It's flexible Ruby wrapper over smtpmock binary.

Example of usage

First, you should install smtp_mock gem and smtpmock as system dependency:

gem install smtp_mock
bundle exec smtp_mock -i ~

Now, you can create and interact with your smtpmock instance natively from Ruby ecosystem. It comes with default settings out of the box. Configure only what you need, for example:

require 'smtp_mock'

# List of all available server options:
# https://github.com/mocktools/ruby-smtp-mock#available-server-options
smtp_mock_server = SmtpMock.start_server(not_registered_emails: %w[[email protected] [email protected]])

# returns current smtp mock server port
smtp_mock_server.port # => 55640

# interface for force shutdown current smtp mock server
smtp_mock_server.stop! # => true

Inside of any ecosystem

You can use smtpmock as binary. Just download the pre-compiled binary from the releases page and copy them to the desired location. For start server run command with needed arguments. You can use our bash script for automation this process like in the example below:

curl -sL https://raw.githubusercontent.com/mocktools/go-smtp-mock/master/script/download.sh | bash
./smtpmock -port=2525 -log

Configuring with command line arguments

smtpmock configuration is available as command line arguments specified in the list below:

Flag description Example of usage
-host - host address where smtpmock will run. It's equal to 127.0.0.1 by default -host=localhost
-port - server port number. If not specified it will be assigned dynamically -port=2525
-log - enables log server activity. Disabled by default -log
-sessionTimeout - session timeout in seconds. It's equal to 30 seconds by default -sessionTimeout=60
-shutdownTimeout - graceful shutdown timeout in seconds. It's equal to 1 second by default -shutdownTimeout=5
-failFast - enables fail fast scenario. Disabled by default -failFast
-multipleRcptto - enables multiple RCPT TO receiving scenario. Disabled by default -multipleRcptto
-multipleMessageReceiving - enables multiple message receiving scenario. Disabled by default -multipleMessageReceiving
-blacklistedHeloDomains - blacklisted HELO domains, separated by commas -blacklistedHeloDomains="example1.com,example2.com"
-blacklistedMailfromEmails - blacklisted MAIL FROM emails, separated by commas -blacklistedMailfromEmails="[email protected],[email protected]"
-blacklistedRcpttoEmails - blacklisted RCPT TO emails, separated by commas -blacklistedRcpttoEmails="[email protected],[email protected]"
-notRegisteredEmails - not registered (non-existent) RCPT TO emails, separated by commas -notRegisteredEmails="[email protected],[email protected]"
-responseDelayHelo - HELO response delay in seconds. It's equal to 0 seconds by default -responseDelayHelo=2
-responseDelayMailfrom - MAIL FROM response delay in seconds. It's equal to 0 seconds by default -responseDelayMailfrom=2
-responseDelayRcptto - RCPT TO response delay in seconds. It's equal to 0 seconds by default -responseDelayRcptto=2
-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"
-msgInvalidCmd - custom invalid command message -msgInvalidCmd="Invalid command message"
-msgInvalidCmdHeloSequence - custom invalid command HELO sequence message -msgInvalidCmdHeloSequence="Invalid command HELO sequence message"
-msgInvalidCmdHeloArg - custom invalid command HELO argument message -msgInvalidCmdHeloArg="Invalid command HELO argument message"
-msgHeloBlacklistedDomain - custom HELO blacklisted domain message -msgHeloBlacklistedDomain="Blacklisted domain message"
-msgHeloReceived - custom HELO received message -msgHeloReceived="HELO received message"
-msgInvalidCmdMailfromSequence - custom invalid command MAIL FROM sequence message -msgInvalidCmdMailfromSequence="Invalid command MAIL FROM sequence message"
-msgInvalidCmdMailfromArg - custom invalid command MAIL FROM argument message -msgInvalidCmdMailfromArg="Invalid command MAIL FROM argument message"
-msgMailfromBlacklistedEmail - custom MAIL FROM blacklisted email message -msgMailfromBlacklistedEmail="Blacklisted email message"
-msgMailfromReceived- custom MAIL FROM received message -msgMailfromReceived="MAIL FROM received message"
-msgInvalidCmdRcpttoSequence - custom invalid command RCPT TO sequence message -msgInvalidCmdRcpttoSequence="Invalid command RCPT TO sequence message"
-msgInvalidCmdRcpttoArg - custom invalid command RCPT TO argument message -msgInvalidCmdRcpttoArg="Invalid command RCPT TO argument message"
-msgRcpttoNotRegisteredEmail - custom RCPT TO not registered email message -msgRcpttoNotRegisteredEmail="Not registered email message"
-msgRcpttoBlacklistedEmail - custom RCPT TO blacklisted email message -msgRcpttoBlacklistedEmail="Blacklisted email message"
-msgRcpttoReceived - custom RCPT TO received message -msgRcpttoReceived="RCPT TO received message"
-msgInvalidCmdDataSequence - custom invalid command DATA sequence message -msgInvalidCmdDataSequence="Invalid command DATA sequence message"
-msgDataReceived - custom DATA received message -msgDataReceived="DATA received message"
-msgMsgSizeIsTooBig - custom size is too big message -msgMsgSizeIsTooBig="Message size is too big"
-msgMsgReceived - custom received message body message -msgMsgReceived="Message has been received"
-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"
-msgNoopReceived - custom NOOP received message -msgNoopReceived="NOOP received message"
-msgQuitCmd - custom QUIT command message -msgQuitCmd="Quit command message"

Other options

Available not configuration smtpmock options:

Flag description Example of usage
-v - Just prints current smtpmock binary build data (version, commit, datetime). Doesn't run the server. -v

Stopping server

smtpmock accepts 3 shutdown signals: SIGINT, SIGQUIT, SIGTERM.

Implemented SMTP commands

id Command Sequenceable Available args Example of usage
1 HELO no domain name, localhost, ip address, [ip address] HELO example.com
1 EHLO no domain name, localhost, ip address, [ip address] EHLO example.com
2 MAIL FROM can be used after command with id 1 and greater email address, <email address>, localhost email address, <localhost email address> MAIL FROM: [email protected]
3 RCPT TO can be used after command with id 2 and greater email address, <email address>, localhost email address, <localhost email address> RCPT TO: [email protected]
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 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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/mocktools/go-smtp-mock. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct. Please check the open tickets. Be sure to follow Contributor Code of Conduct below and our Contributing Guidelines.

License

This golang package is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the smtpmock projectโ€™s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Credits

Versioning

smtpmock uses Semantic Versioning 2.0.0

go-smtp-mock's People

Contributors

bestwebua avatar dandare100 avatar mitar avatar srishti-nema avatar vpakhuchyi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

go-smtp-mock's Issues

[FEATURE] Ability to use address literal as valid HELO command argument

New feature checklist

Feature description

Ability to use address literal as valid HELO command argument, for example: ehlo [192.168.1.1]. Current version of go-smtp-mock for this request responds with:

220 Welcome
ehlo [192.168.1.1]
501 HELO requires domain address  

According to https://datatracker.ietf.org/doc/html/rfc2821#section-4.1.1.1 the mail client SHOULD send an address literal if the client system does not have a meaningful domain name.

[FEATURE] Support multiple RCPT TO addresses on the same session

Please could we add the ability to specify multiple recipients by being able to send the RCPT TO: multiple times with different addresses on the same session.

At the moment the framework simply replaces the last rcpt address with the newly requested on

It should be ok because this is how the rfc defines it : https://datatracker.ietf.org/doc/html/rfc2821#section-4.1.1.3

Perhaps we can change the rcpttoRequest type in the Message struct to a slice and append the to address of every RCPT TO:

I am happy to have a go, I am just checking in with the idea.

[FEATURE] Support for multiple mails over one connection

New feature request checklist

Feature description

It should be possible to send multiple mails over one connection. At the moment only the last mail will be
returned when using Server.Messages()

Code to reproduce

package main

import (
	"fmt"
	"github.com/mocktools/go-smtp-mock"
	mail "github.com/xhit/go-simple-mail/v2"

	"os"
)

func main() {
	SMTPServer := smtpmock.New(smtpmock.ConfigurationAttr{
		LogToStdout:       true,
		LogServerActivity: true,
	})

	if err := SMTPServer.Start(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	srv := mail.NewSMTPClient()
	srv.Host = "127.0.0.1"
	srv.Port = SMTPServer.PortNumber

	// don't close the connection
	srv.KeepAlive = true
	client, _ := srv.Connect()

	// mail no.1
	email := mail.NewMSG()
	email.SetFrom("[email protected]").
		AddTo("[email protected]").
		SetSubject("subject").
		SetBody(mail.TextHTML, "HTML-body").
		AddAlternative(mail.TextPlain, "TXT-alternative")
	email.Send(client)

	// mail no.2
	email.SetSubject("subject2")
	email.SetFrom("[email protected]")
	email.Send(client)

	mails := SMTPServer.Messages()
	fmt.Printf("Number of messages %d \n", len(mails))
	fmt.Printf("mailfrom: %s \n", mails[0].MailfromRequest())
}

Output:

INFO: 2022/06/24 07:56:34.791896 SMTP mock server started on port: 34239
INFO: 2022/06/24 07:56:34.792423 SMTP session started
INFO: 2022/06/24 07:56:34.792550 SMTP response: 220 Welcome
INFO: 2022/06/24 07:56:34.792788 SMTP request: EHLO localhost
INFO: 2022/06/24 07:56:34.794501 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.794822 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/06/24 07:56:34.795237 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.795317 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/06/24 07:56:34.795858 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.795870 SMTP request: DATA
INFO: 2022/06/24 07:56:34.795893 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/06/24 07:56:34.795907 SMTP request: message binary data portion
...
INFO: 2022/06/24 07:56:34.795927 SMTP request: message binary data portion
INFO: 2022/06/24 07:56:34.795931 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.795940 SMTP request: RSET
INFO: 2022/06/24 07:56:34.795952 SMTP response: 502 Command unrecognized. Available commands: HELO, EHLO, MAIL FROM:, RCPT TO:, DATA, QUIT
INFO: 2022/06/24 07:56:34.796038 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/06/24 07:56:34.796401 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.796509 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/06/24 07:56:34.796970 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.796984 SMTP request: DATA
INFO: 2022/06/24 07:56:34.797007 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/06/24 07:56:34.797018 SMTP request: message binary data portion
...
INFO: 2022/06/24 07:56:34.797047 SMTP request: message binary data portion
INFO: 2022/06/24 07:56:34.797051 SMTP response: 250 Received
INFO: 2022/06/24 07:56:34.797063 SMTP request: RSET
INFO: 2022/06/24 07:56:34.797071 SMTP response: 502 Command unrecognized. Available commands: HELO, EHLO, MAIL FROM:, RCPT TO:, DATA, QUIT
Number of messages 1 
mailfrom: MAIL FROM:<[email protected]> 

The problem exists because command RSET is not implemented.

[FEATURE] Easy asserts for mail data

New feature request checklist

Feature description

When testing an email delivery, I also want to be sure, that the inner things of an email are correct. For this I am currently missing the suitable getters.

I would like to test

Content Type (text/plain or text/html)

assert.Equal(t, "text/html", server.Messages()[0].ContentType())

Content contains a certain string

assert.Contains(t, "Sample text", server.Messages()[0].Content())

Subject is/contains a certain string

assert.Contains(t, "Example", server.Messages()[0].Subject())
assert.Equals(t, "Example", server.Messages()[0].Subject())

[BUG] Email address with name is considered invalid

New bug checklist

Bug description

I am trying to send an email with this sender address John Doe<[email protected]>. I think this should also be a valid email address format to be supported by smtpmock.

Currently I am getting back this error: SMTP response: 501 MAIL FROM requires valid email address

This applies to both, sender and receiver address.

Complete output when running smtpmock, including the stack trace and command used
server := smtpmock.New(smtpmock.ConfigurationAttr{
  PortNumber: 2525,
  LogToStdout:       true,
  LogServerActivity: true,
})

if err := server.Start(); err != nil {
  t.Fatal(err)
}
INFO: 2023/02/18 22:03:23.182796 SMTP response: 220 Welcome
truetime="2023-02-18T22:03:23+01:00" level=info msg="Sending mail to SMTP"
INFO: 2023/02/18 22:03:23.182969 SMTP request: EHLO localhost
INFO: 2023/02/18 22:03:23.183380 SMTP response: 250 Received
INFO: 2023/02/18 22:03:23.183462 SMTP request: MAIL FROM:<John Doe<[email protected]>>
INFO: 2023/02/18 22:03:23.183590 SMTP response: 501 MAIL FROM requires valid email address
time="2023-02-18T22:03:23+01:00" level=error msg="MAIL FROM requires valid email address"

[QUESTION] Increase test coverage for handler type

Need more test coverage on handler type

Where?

Defined in https://github.com/mocktools/go-smtp-mock/blob/master/handler.go
and all uses of the handler type in handler_*.go files

New issue checklist

Issue description

There is insufficient test coverage on handler and where it is embedded (e.g. in handlerRset). I was able to break it with just one change in #125 - which was fixed in #127.
If I could break something like this so easily without a test failing then it's clear we don't have the essential unit test coverage.

[BUG] BrokenImport, could not import (no required module provides package)

New bug checklist

Bug description

When following the instructions on installing / importing in Go, I'm running into the following issue...

  1. Install smtpmock:
go get github.com/mocktools/go-smtp-mock/v2
go install github.com/mocktools/go-smtp-mock/v2/smtpmock@latest
Output: go: github.com/mocktools/go-smtp-mock/v2/smtpmock@latest: module github.com/mocktools/go-smtp-mock/v2@latest found (v2.0.1), but does not contain package github.com/mocktools/go-smtp-mock/v2/smtpmock
  1. Import statement error in go package:
compiler: could not import github.com/mocktools/go-smtp-mock/v2 (no required module provides package "github.com/mocktools/go-smtp-mock/v2") [BrokenImport]

Am I doing something obviously wrong?

I'm using go version go1.19.3 linux/amd64

[FEATURE] Ability to use localhost as valid HELO domain

Getting this strange error when trying to send email to the server

INFO: 2021/12/13 00:39:13.629342 SMTP mock server started on port: 2525
INFO: 2021/12/13 00:39:23.666279 SMTP session started
INFO: 2021/12/13 00:39:23.666279 SMTP response: 220 Welcome
INFO: 2021/12/13 00:39:23.667275 SMTP request: EHLO localhost
INFO: 2021/12/13 00:39:23.668414 SMTP response: 501 HELO requires domain address
INFO: 2021/12/13 00:39:23.669274 SMTP request: HELO localhost
INFO: 2021/12/13 00:39:23.670276 SMTP response: 501 HELO requires domain address
2021/12/13 00:39:23 501 HELO requires domain address

Code to start the server:

server := smtpmock.New(smtpmock.ConfigurationAttr{
		LogToStdout:       true,
		LogServerActivity: true,
		PortNumber:        2525,
		HostAddress:       "127.0.0.1",
	})

	// To start server use Start() method
	if err := server.Start(); err != nil {
		fmt.Println(err)
	}

[BUG] RSET resets already sent message

New bug checklist

Bug description

I think implementation of handling RSET resets too much. So my client (go-mail) tries to keep alive connection to the server. So after successful sending of a message it resets the connections and uses NOOP to keep it alive. But RSET unconditionally clears the message. I think this is wrong. So after message after DATA is acknowledged to the client, RSET should not reset it anymore. This SO answers seem to confirm this.

Output log from my Go program:

INFO: 2024/02/27 11:17:04.757673 SMTP mock server started on port: 35997
INFO: 2024/02/27 11:17:05.414803 SMTP session started
INFO: 2024/02/27 11:17:05.414848 SMTP response: 220 Welcome
INFO: 2024/02/27 11:17:05.414879 SMTP request: EHLO localhost
INFO: 2024/02/27 11:17:05.415460 SMTP response: 250 Received
INFO: 2024/02/27 11:17:05.415510 SMTP request: NOOP
INFO: 2024/02/27 11:17:05.415558 SMTP response: 250 Ok
INFO: 2024/02/27 11:17:05.415579 SMTP request: NOOP
INFO: 2024/02/27 11:17:05.415621 SMTP response: 250 Ok
INFO: 2024/02/27 11:17:05.415639 SMTP request: MAIL FROM:<[email protected]>
INFO: 2024/02/27 11:17:05.416070 SMTP response: 250 Received
INFO: 2024/02/27 11:17:05.416096 SMTP request: RCPT TO:<[email protected]>
INFO: 2024/02/27 11:17:05.416707 SMTP response: 250 Received
INFO: 2024/02/27 11:17:05.416732 SMTP request: DATA
INFO: 2024/02/27 11:17:05.417103 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2024/02/27 11:17:05.417201 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417208 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417210 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417244 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417256 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417266 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417275 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417284 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417300 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417310 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417319 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417327 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417336 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417345 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417353 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417362 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417376 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417385 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417394 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417402 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417411 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417419 SMTP request: message binary data portion
INFO: 2024/02/27 11:17:05.417441 SMTP response: 250 Received
INFO: 2024/02/27 11:17:05.417473 SMTP request: NOOP
INFO: 2024/02/27 11:17:05.417552 SMTP response: 250 Ok
INFO: 2024/02/27 11:17:05.417623 SMTP request: RSET
WARNING: 2024/02/27 11:17:20.428925 SMTP mock server is in the shutdown mode and won't accept new connections
INFO: 2024/02/27 11:17:21.430001 SMTP mock server was force stopped by timeout

[BUG] Race condition detected when run in test files

New bug checklist

Bug description

Race condition detected when running tests in file

Test file:

package mailer

import (
	"fmt"
	"log"
	"net/smtp"
	"testing"
	"time"

	"bitbucket.org/vadacom/commander/pkg/logging"
	smtpmock "github.com/mocktools/go-smtp-mock"
)

func last[E any](s []E) E {
	return s[len(s)-1]
}

var (
	smtpmockServer        *smtpmock.Server
	smtpmockServerAddress string
)

func TestMain(m *testing.M) {
	// You can pass empty smtpmock.ConfigurationAttr{}. It means that smtpmock will use default settings
	smtpmockServer = smtpmock.New(smtpmock.ConfigurationAttr{
		LogToStdout:       true,
		LogServerActivity: true,
	})

	// To start server use Start() method
	if err := smtpmockServer.Start(); err != nil {
		log.Panic(err)
	}
	defer smtpmockServer.Stop()
	// Server's port will be assigned dynamically after server.Start()
	// for case when portNumber wasn't specified
	hostAddress, portNumber := "127.0.0.1", smtpmockServer.PortNumber

	// Possible SMTP-client stuff for iteration with mock server
	smtpmockServerAddress = fmt.Sprintf("%s:%d", hostAddress, portNumber)

	exit := m.Run()
	log.Print(exit)
}

func TestSMTPTransport_Send(t *testing.T) {
	l := logging.TestingStructuredNop(t)
	mailTransport, err := NewSMTPTransport(
		l,
		smtpmockServerAddress,
		smtp.Auth(nil),
		30*time.Second,  // dial timeout
		30*time.Second,  // command timeout
		300*time.Second, // DATA timeout
	)
	if err != nil {
		t.Fatal(err)
	}

	const (
		someoneEmailAddress = "[email protected]"
		peanutEmailAddress  = "[email protected]"
		benEmailAddress     = "[email protected]"
		mailFromTmpl        = "MAIL FROM:<%v>"
		rcptToTmpl          = "RCPT TO:<%v>"
		msgBodyFooter       = "\r\n"
	)
	emptyFromAddressErr := fmt.Errorf("from address ('') parsing failed: mail: no address")
	emptyToAddressErr := fmt.Errorf("to address ('') parsing failed: mail: no address")
	missingToAddresses := fmt.Errorf("missing email to addressees")
	missingEmailMsg := fmt.Errorf("missing email message body")
	helloEmailMsg := []byte("Hello World")

	type args struct {
		email_from_address string
		email_to_addresses []string
		email_msg          []byte
	}
	tests := []struct {
		name    string
		args    args
		wantErr error
	}{
		{name: "MUST FAIL: empty from address", args: args{"", []string{someoneEmailAddress}, nil}, wantErr: emptyFromAddressErr},
		{name: "MUST FAIL: empty to address", args: args{someoneEmailAddress, []string{""}, nil}, wantErr: emptyToAddressErr},
		{name: "MUST FAIL: nil list of to addresses", args: args{someoneEmailAddress, nil, nil}, wantErr: missingToAddresses},
		{name: "MUST FAIL: empty list of to addresses", args: args{someoneEmailAddress, []string{}, nil}, wantErr: missingToAddresses},
		{name: "MUST FAIL: nil message body", args: args{someoneEmailAddress, []string{someoneEmailAddress}, nil}, wantErr: missingEmailMsg},
		{name: "MUST FAIL: empty message body", args: args{someoneEmailAddress, []string{someoneEmailAddress}, []byte{}}, wantErr: missingEmailMsg},
		{name: "hello world email from someone to someone", args: args{someoneEmailAddress, []string{someoneEmailAddress}, helloEmailMsg}, wantErr: nil},
		{name: "hello world email from ben to someone", args: args{benEmailAddress, []string{someoneEmailAddress}, helloEmailMsg}, wantErr: nil},
		{name: "hello world email from someone to multiple addresses", args: args{someoneEmailAddress, []string{benEmailAddress, peanutEmailAddress}, helloEmailMsg}, wantErr: nil},
		{name: "hello world email from ben to multiple addresses", args: args{benEmailAddress, []string{peanutEmailAddress, someoneEmailAddress}, helloEmailMsg}, wantErr: nil},
		{name: "hello world email from peanut to multiple addresses", args: args{peanutEmailAddress, []string{someoneEmailAddress, benEmailAddress}, helloEmailMsg}, wantErr: nil},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := mailTransport.Send(tt.args.email_from_address, tt.args.email_to_addresses, tt.args.email_msg)
			if err != tt.wantErr {
				if err != nil && tt.wantErr != nil {
					if err.Error() != tt.wantErr.Error() {
						t.Errorf("smtp transport send failed unexpectedly, expected: %v, got: %v", tt.wantErr, err)
						return
					}
				} else {
					t.Errorf("smtp transport send failed unexpectedly, expected: %v, got: %v", tt.wantErr, err)
					return
				}
			}
			if tt.wantErr == nil {
				// check result of last email send
				messagesFromMockSmtpServer := smtpmockServer.Messages()
				message := last(messagesFromMockSmtpServer)
				expectedMailFrom := fmt.Sprintf(mailFromTmpl, tt.args.email_from_address)
				actualMailFrom := message.MailfromRequest()
				if expectedMailFrom != actualMailFrom {
					t.Errorf("unexpected email message sender, expected: %q, got: %q", expectedMailFrom, actualMailFrom)
					return
				}
				// FIXME: go-smtp-mock implementation is broken, only records last recipient address per message
				// see github issue: https://github.com/mocktools/go-smtp-mock/issues/120
				expectedMailRcpt := fmt.Sprintf(rcptToTmpl, last(tt.args.email_to_addresses))
				actualMailRcpt := message.RcpttoRequest()
				if expectedMailRcpt != actualMailRcpt {
					t.Errorf("unexpected email message recipient, expected: %q, got: %q", expectedMailRcpt, actualMailRcpt)
					return
				}
				// "\r\n" added automatically in sending process
				expectedMsgBody := string(tt.args.email_msg) + msgBodyFooter
				actualMsgBody := message.MsgRequest()
				if expectedMsgBody != actualMsgBody {
					t.Errorf("unexpected email message body, expected: %q, got: %q", expectedMsgBody, actualMsgBody)
					return
				}
			}
		})
	}
}

// EOF
Complete output when running smtpmock, including the stack trace and command used
โฏ go test -race /w/v/r/C/internal/mailer
INFO: 2022/11/10 17:05:50.796925 SMTP mock server started on port: 43145
INFO: 2022/11/10 17:05:50.838546 SMTP session started
INFO: 2022/11/10 17:05:50.838594 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.849022 SMTP session started
INFO: 2022/11/10 17:05:50.849099 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.859598 SMTP session started
INFO: 2022/11/10 17:05:50.859691 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.859791 SMTP request: EHLO localhost
INFO: 2022/11/10 17:05:50.863678 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.863719 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/11/10 17:05:50.866715 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.866744 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.871119 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.871142 SMTP request: DATA
INFO: 2022/11/10 17:05:50.871240 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/11/10 17:05:50.871267 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.871275 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.871288 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.871308 SMTP request: QUIT
INFO: 2022/11/10 17:05:50.871391 SMTP response: 221 Closing connection
INFO: 2022/11/10 17:05:50.871418 SMTP session finished
INFO: 2022/11/10 17:05:50.881919 SMTP session started
==================
WARNING: DATA RACE
Write at 0x00c0001d8ac8 by goroutine 27:
  github.com/mocktools/go-smtp-mock.(*messages).append()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/message.go:139 +0x164
  github.com/mocktools/go-smtp-mock.(*Server).newMessage()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:127 +0xfd
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:170 +0xfe
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

Previous read at 0x00c0001d8ac8 by goroutine 21:
  github.com/mocktools/go-smtp-mock.(*Server).Messages()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:121 +0x2d6
  bitbucket.org/vxxxxxxx/cxxxxxxxx/internal/mailer.TestSMTPTransport_Send.func1()
      /wrk/vada/r/CMDR/internal/mailer/smtp_transport_test.go:119 +0x292
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1439 +0x213
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1486 +0x47

Goroutine 27 (running) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae

Goroutine 21 (finished) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1486 +0x724
  bitbucket.org/vxxxxxxx/cxxxxxxxx/internal/mailer.TestSMTPTransport_Send()
      /wrk/vada/r/CMDR/internal/mailer/smtp_transport_test.go:99 +0xec4
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1439 +0x213
  testing.(*T).Run.func1()
      /usr/local/go/src/testing/testing.go:1486 +0x47
==================
INFO: 2022/11/10 17:05:50.882110 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.882191 SMTP request: EHLO localhost
INFO: 2022/11/10 17:05:50.885873 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.885935 SMTP request: MAIL FROM:<[email protected]>
ERROR: 2022/11/10 17:05:50.889121 read tcp 127.0.0.1:43145->127.0.0.1:49552: read: connection reset by peer
==================
WARNING: DATA RACE
Read at 0x00c0000b87e0 by goroutine 16:
  github.com/mocktools/go-smtp-mock.(*eventLogger).error()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/logger.go:78 +0x64
  github.com/mocktools/go-smtp-mock.(*session).readRequest()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/session.go:119 +0xe7
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:179 +0x204
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

INFO: 2022/11/10 17:05:50.889147 SMTP session finished
Previous write at 0x00c0000b87e0 by goroutine 19:
  github.com/mocktools/go-smtp-mock.(*eventLogger).error()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/logger.go:79 +0x204
  github.com/mocktools/go-smtp-mock.(*session).readRequest()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/session.go:119 +0xe7
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:179 +0x204
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

Goroutine 16 (running) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae

Goroutine 19 (running) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae
==================
==================
WARNING: DATA RACE
Read at 0x00c0003300e8 by goroutine 16:
  sync/atomic.LoadInt32()
      /usr/local/go/src/runtime/race_amd64.s:200 +0xb
  sync/atomic.LoadInt32()
      <autogenerated>:1 +0x16
  github.com/mocktools/go-smtp-mock.(*eventLogger).error()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/logger.go:82 +0x284
  github.com/mocktools/go-smtp-mock.(*session).readRequest()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/session.go:119 +0xe7
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:179 +0x204
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

Previous write at 0x00c0003300e8 by goroutine 19:
  log.New()
      /usr/local/go/src/log/log.go:68 +0xc6
  github.com/mocktools/go-smtp-mock.(*eventLogger).error()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/logger.go:79 +0x75
  github.com/mocktools/go-smtp-mock.(*session).readRequest()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/session.go:119 +0xe7
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:179 +0x204
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

Goroutine 16 (running) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae

Goroutine 19 (finished) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae
==================
==================
WARNING: DATA RACE
Write at 0x00c0003300a0 by goroutine 16:
  sync/atomic.CompareAndSwapInt32()
      /usr/local/go/src/runtime/race_amd64.s:308 +0xb
  sync/atomic.CompareAndSwapInt32()
      <autogenerated>:1 +0x1e
  log.(*Logger).Output()
      /usr/local/go/src/log/log.go:176 +0x88
  log.(*Logger).Println()
      /usr/local/go/src/log/log.go:223 +0x84
  github.com/mocktools/go-smtp-mock.(*eventLogger).error()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/logger.go:82 +0x284
  github.com/mocktools/go-smtp-mock.(*session).readRequest()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/session.go:119 +0xe7
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:179 +0x204
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

Previous write at 0x00c0003300a0 by goroutine 19:
  log.New()
      /usr/local/go/src/log/log.go:68 +0xc6
  github.com/mocktools/go-smtp-mock.(*eventLogger).error()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/logger.go:79 +0x75
  github.com/mocktools/go-smtp-mock.(*session).readRequest()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/session.go:119 +0xe7
  github.com/mocktools/go-smtp-mock.(*Server).handleSession()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:179 +0x204
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1.1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:80 +0x7b

Goroutine 16 (running) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae

Goroutine 19 (finished) created at:
  github.com/mocktools/go-smtp-mock.(*Server).Start.func1()
      /home/br/go/pkg/mod/github.com/mocktools/[email protected]/server.go:79 +0xae
==================
ERROR: 2022/11/10 17:05:50.889228 read tcp 127.0.0.1:43145->127.0.0.1:49546: read: connection reset by peer
INFO: 2022/11/10 17:05:50.889303 SMTP session finished
INFO: 2022/11/10 17:05:50.890609 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.890655 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.895050 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.895092 SMTP request: DATA
INFO: 2022/11/10 17:05:50.895190 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/11/10 17:05:50.895226 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.895236 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.895250 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.895284 SMTP request: QUIT
INFO: 2022/11/10 17:05:50.895368 SMTP response: 221 Closing connection
INFO: 2022/11/10 17:05:50.895387 SMTP session finished
INFO: 2022/11/10 17:05:50.905903 SMTP session started
INFO: 2022/11/10 17:05:50.905951 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.906008 SMTP request: EHLO localhost
INFO: 2022/11/10 17:05:50.909605 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.909655 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/11/10 17:05:50.912594 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.912629 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.916907 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.916953 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.921127 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.921170 SMTP request: DATA
INFO: 2022/11/10 17:05:50.921270 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/11/10 17:05:50.921296 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.921306 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.921318 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.921348 SMTP request: QUIT
INFO: 2022/11/10 17:05:50.921426 SMTP response: 221 Closing connection
INFO: 2022/11/10 17:05:50.921442 SMTP session finished
INFO: 2022/11/10 17:05:50.931890 SMTP session started
INFO: 2022/11/10 17:05:50.931967 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.932040 SMTP request: EHLO localhost
INFO: 2022/11/10 17:05:50.936743 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.936792 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/11/10 17:05:50.941325 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.941369 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.947843 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.947883 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.954301 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.954346 SMTP request: DATA
INFO: 2022/11/10 17:05:50.954527 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/11/10 17:05:50.954566 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.954579 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.954599 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.954634 SMTP request: QUIT
INFO: 2022/11/10 17:05:50.954759 SMTP response: 221 Closing connection
INFO: 2022/11/10 17:05:50.954802 SMTP session finished
INFO: 2022/11/10 17:05:50.965330 SMTP session started
INFO: 2022/11/10 17:05:50.965423 SMTP response: 220 Welcome
INFO: 2022/11/10 17:05:50.965479 SMTP request: EHLO localhost
INFO: 2022/11/10 17:05:50.971722 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.971760 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/11/10 17:05:50.977719 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.977756 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.987160 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.987220 SMTP request: RCPT TO:<[email protected]>
INFO: 2022/11/10 17:05:50.996182 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.996260 SMTP request: DATA
INFO: 2022/11/10 17:05:50.996443 SMTP response: 354 Ready for receive message. End data with <CR><LF>.<CR><LF>
INFO: 2022/11/10 17:05:50.996507 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.996523 SMTP request: message binary data portion
INFO: 2022/11/10 17:05:50.996551 SMTP response: 250 Received
INFO: 2022/11/10 17:05:50.996608 SMTP request: QUIT
INFO: 2022/11/10 17:05:50.996802 SMTP response: 221 Closing connection
INFO: 2022/11/10 17:05:50.996861 SMTP session finished
--- FAIL: TestSMTPTransport_Send (0.21s)
    --- FAIL: TestSMTPTransport_Send/hello_world_email_from_ben_to_someone (0.02s)
        testing.go:1312: race detected during execution of test
    testing.go:1312: race detected during execution of test
FAIL
2022/11/10 17:05:51 1
WARNING: 2022/11/10 17:05:51.047892 SMTP mock server is in the shutdown mode and won't accept new connections
INFO: 2022/11/10 17:05:51.047984 SMTP mock server was stopped successfully
FAIL	bitbucket.org/vxxxxxxx/cxxxxxxxx/internal/mailer	0.262s
FAIL

[BUG] Invalid email can be passed as argument for MAIL FROM, RCPT TO commands

New bug checklist

Bug description

Regex for validation of email works in wrong way. We can pass to MAIL FROM, RCPT TO invalid email, like [email protected]@b.com. Seems when wrong email ends on valid domain it passes ([email protected]@b.com). We need to fix it and cover via tests.

Complete output when running smtpmock, including the stack trace and command used
INFO: 2022/11/18 19:20:12.419178 SMTP session started
INFO: 2022/11/18 19:20:12.419265 SMTP response: 220 Welcome
INFO: 2022/11/18 19:20:12.419533 SMTP request: EHLO gibson.biz
INFO: 2022/11/18 19:20:12.421156 SMTP response: 250 Received
INFO: 2022/11/18 19:20:12.421370 SMTP request: MAIL FROM:<[email protected]>
INFO: 2022/11/18 19:20:12.421935 SMTP response: 250 Received
INFO: 2022/11/18 19:20:12.422099 SMTP request: RCPT TO:<[email protected]@b.com>
INFO: 2022/11/18 19:20:12.422823 SMTP response: 250 Received
INFO: 2022/11/18 19:20:12.422972 SMTP request: DATA

[BUG] Cannot use localhost as domain in MAIL FROM

Using v2.0.5 doesn't accept localhost as valid for MAIL FROM addresses

Expected result (using postfix):

220 gokyo.local ESMTP Postfix
HELO localhost
250 gokyo.local
MAIL FROM: <dev@localhost>
250 2.1.0 Ok

Result with smtpmock 2.0.5:

220 Welcome
HELO localhost
250 Received
MAIL FROM: <dev@localhost>
501 MAIL FROM requires valid email address

[FEATURE] Optional message purge when retrieving messages

I understand that this is not a fully fledged mail server and it is meant and optimized for mocking.

From my side it would be cool to have a method
func (server *Server) MessagesAndPurge() []*Message

Would you entertain such a request ?

Describe the solution you'd like. A clear and concise description of what you want to happen.

Add method
func (server *Server) MessagesAndPurge() []*Message
which returns all the messages that are no longer being handled in a session, and removes them from the slice.

We need to consider messages that are currently being modified in active sessions. We would need a way of telling if a message is busy being handled in a session because it should not be removed. Perhaps a marker variable that gets updated when the the session ends ?

Even "inconsistent" messages should be returned and purged.

Just an idea, please forgive brevity

[BUG] 2.3.0 broke message reading...

New bug checklist

Bug description

Upgraded from v2.2.1 to v2.3.0 and our tests started failing.
Our setup is quite simple. We use it as a mock during testing. We start the mock server on TestMain() and use it on the tests. The test consist on sending a mail using the standard net/smtp using the mock server address. Then reading the messages, finding the message we want and doing some assertions.

We couldn't find the expected messages using .Messages().

I suspects there is a race condition there. Since the tests do pass locally, but they fail during CI (probably due to a slower machine on CI). I added a quick time.Sleep(time.Millisecond) before accessing .Messages() and that caused the tests to pass again.

[FEATURE] Logging to testing logger

New feature request checklist

Feature description

Currently this package supports logging to stdout. Which works well, but I think inside unit test it would be even better if it would support logging to testing.Logf. Then it is easier to see chronological order of logging messages, when other parts of your unit test are using testing.Logf. They are also shown only if the test fails.

I think API could be an option to ConfigurationAttr, something like LogToTesting *testing.T and you would pass *testing.T instance to it.

Alternative would be that I could pass my own "logger" instance and then I could make my own adapter.

[BUG] Data race between `newMessage()` and `Messages()`

New bug checklist

Bug description

Messages() call protected with:

	server.Lock()
	defer server.Unlock()

but newMessage() doesn't. It looks like because of this we can have following race condition:

WARNING: DATA RACE
Read at 0x00c00024a950 by goroutine 9:
  github.com/mocktools/go-smtp-mock/v2.(*Server).Messages()
      /home/anton/go/pkg/mod/github.com/mocktools/go-smtp-mock/[email protected]/server.go:128 +0x1ab
...
Previous write at 0x00c00024a950 by goroutine 25:
  github.com/mocktools/go-smtp-mock/v2.(*messages).append()
      /home/anton/go/pkg/mod/github.com/mocktools/go-smtp-mock/[email protected]/message.go:154 +0x11b
  github.com/mocktools/go-smtp-mock/v2.(*Server).newMessage()
      /home/anton/go/pkg/mod/github.com/mocktools/go-smtp-mock/[email protected]/server.go:181 +0xfd
...

Please kindly let me know if direction of my thoughts is right, or there is some other reason for this race?
Thanks in advance!

Complete output when running smtpmock, including the stack trace and command used
[INSERT OUTPUT HERE]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.