package module
Version: v1.0.1 Latest Latest

This package is not in the latest version of its module.

Go to latest
Published: Nov 19, 2016 License: MIT Imports: 7 Imported by: 28


MailHog SMTP Protocol GoDoc Build Status implements an SMTP server state machine.

It attempts to encapsulate as much of the SMTP protocol (plus its extensions) as possible without compromising configurability or requiring specific backend implementations.

proto := NewProtocol()
reply := proto.Start()
reply = proto.ProcessCommand("EHLO localhost")
// ...

See MailHog-Server and MailHog-MTA for example implementations.

Commands and replies

Interaction with the state machine is via:

  • the Parse function
  • the ProcessCommand and ProcessData functions

You can mix the use of all three functions as necessary.


Parse should be used on a raw text stream. It looks for an end of line (\r\n), and if found, processes a single command. Any unprocessed data is returned.

If any unprocessed data is returned, Parse should be called again to process then next command.

text := "EHLO localhost\r\nMAIL FROM:<test>\r\nDATA\r\nTest\r\n.\r\n"

var reply *smtp.Reply
for {
  text, reply = proto.Parse(text)
  if len(text) == 0 {
ProcessCommand and ProcessData

ProcessCommand should be used for an already parsed command (i.e., a complete SMTP "line" excluding the line ending).

ProcessData should be used if the protocol is in DATA state.

reply = proto.ProcessCommand("EHLO localhost")
reply = proto.ProcessCommand("MAIL FROM:<test>")
reply = proto.ProcessCommand("DATA")
reply = proto.ProcessData("Test\r\n.\r\n")

The state machine provides hooks to manipulate its behaviour.

See GoDoc for more information.

Hook Description
LogHandler Called for every log message
MessageReceivedHandler Called for each message received
ValidateSenderHandler Called after MAIL FROM
ValidateRecipientHandler Called after RCPT TO
ValidateAuthenticationHandler Called after AUTH
SMTPVerbFilter Called for every SMTP command processed
TLSHandler Callback mashup called after STARTTLS
GetAuthenticationMechanismsHandler Called for each EHLO command
Behaviour flags

The state machine also exports variables to control its behaviour:

See GoDoc for more information.

Variable Description
RejectBrokenRCPTSyntax Reject non-conforming RCPT syntax
RejectBrokenMAILSyntax Reject non-conforming MAIL syntax
RequireTLS Require STARTTLS before other commands
MaximumRecipients Maximum recipients per message
MaximumLineLength Maximum length of SMTP line

Copyright ©‎ 2014-2015, Ian Kent (

Released under MIT license, see LICENSE for details.




View Source
const (
	INVALID   = State(-1)
	ESTABLISH = State(iota)

SMTP message conversation states


View Source
var StateMap = map[State]string{
	MAIL:        "MAIL",
	RCPT:        "RCPT",
	DATA:        "DATA",
	DONE:        "DONE",

StateMap provides string representations of SMTP conversation states


This section is empty.


type Command

type Command struct {
	// contains filtered or unexported fields

Command is a struct representing an SMTP command (verb + arguments)

func ParseCommand

func ParseCommand(line string) *Command

ParseCommand returns a Command from the line string

type Protocol

type Protocol struct {
	TLSPending  bool
	TLSUpgraded bool

	State   State
	Message *data.SMTPMessage

	Hostname string
	Ident    string

	MaximumLineLength int
	MaximumRecipients int

	// LogHandler is called for each log message. If nil, log messages will
	// be output using log.Printf instead.
	LogHandler func(message string, args ...interface{})
	// MessageReceivedHandler is called for each message accepted by the
	// SMTP protocol. It must return a MessageID or error. If nil, messages
	// will be rejected with an error.
	MessageReceivedHandler func(*data.SMTPMessage) (string, error)
	// ValidateSenderHandler should return true if the sender is valid,
	// otherwise false. If nil, all senders will be accepted.
	ValidateSenderHandler func(from string) bool
	// ValidateRecipientHandler should return true if the recipient is valid,
	// otherwise false. If nil, all recipients will be accepted.
	ValidateRecipientHandler func(to string) bool
	// ValidateAuthenticationhandler should return true if the authentication
	// parameters are valid, otherwise false. If nil, all authentication
	// attempts will be accepted.
	ValidateAuthenticationHandler func(mechanism string, args ...string) (errorReply *Reply, ok bool)
	// SMTPVerbFilter is called after each command is parsed, but before
	// any code is executed. This provides an opportunity to reject unwanted verbs,
	// e.g. to require AUTH before MAIL
	SMTPVerbFilter func(verb string, args ...string) (errorReply *Reply)
	// TLSHandler is called when a STARTTLS command is received.
	// It should acknowledge the TLS request and set ok to true.
	// It should also return a callback which will be invoked after the reply is
	// sent. E.g., a TCP connection can only perform the upgrade after sending the reply
	// Once the upgrade is complete, invoke the done function (e.g., from the returned callback)
	// If TLS upgrade isn't possible, return an errorReply and set ok to false.
	TLSHandler func(done func(ok bool)) (errorReply *Reply, callback func(), ok bool)

	// GetAuthenticationMechanismsHandler should return an array of strings
	// listing accepted authentication mechanisms
	GetAuthenticationMechanismsHandler func() []string

	// RejectBrokenRCPTSyntax controls whether the protocol accepts technically
	// invalid syntax for the RCPT command. Set to true, the RCPT syntax requires
	// no space between `TO:` and the opening `<`
	RejectBrokenRCPTSyntax bool
	// RejectBrokenMAILSyntax controls whether the protocol accepts technically
	// invalid syntax for the MAIL command. Set to true, the MAIL syntax requires
	// no space between `FROM:` and the opening `<`
	RejectBrokenMAILSyntax bool
	// RequireTLS controls whether TLS is required for a connection before other
	// commands can be issued, applied at the protocol layer.
	RequireTLS bool
	// contains filtered or unexported fields

Protocol is a state machine representing an SMTP session

func NewProtocol

func NewProtocol() *Protocol

NewProtocol returns a new SMTP state machine in INVALID state handler is called when a message is received and should return a message ID

func (*Protocol) Command

func (proto *Protocol) Command(command *Command) (reply *Reply)

Command applies an SMTP verb and arguments to the state machine

func (*Protocol) EHLO

func (proto *Protocol) EHLO(args string) (reply *Reply)

EHLO creates a reply to a EHLO command

func (*Protocol) HELO

func (proto *Protocol) HELO(args string) (reply *Reply)

HELO creates a reply to a HELO command

func (*Protocol) Parse

func (proto *Protocol) Parse(line string) (string, *Reply)

Parse parses a line string and returns any remaining line string and a reply, if a command was found. Parse does nothing until a new line is found. - TODO decide whether to move this to a buffer inside Protocol

sort of like it this way, since it gives control back to the caller

func (*Protocol) ParseMAIL

func (proto *Protocol) ParseMAIL(mail string) (string, error)

ParseMAIL returns the forward-path from a MAIL command argument

func (*Protocol) ParseRCPT

func (proto *Protocol) ParseRCPT(rcpt string) (string, error)

ParseRCPT returns the return-path from a RCPT command argument

func (*Protocol) ProcessCommand

func (proto *Protocol) ProcessCommand(line string) (reply *Reply)

ProcessCommand processes a line of text as a command It expects the line string to be a properly formed SMTP verb and arguments

func (*Protocol) ProcessData

func (proto *Protocol) ProcessData(line string) (reply *Reply)

ProcessData handles content received (with newlines stripped) while in the SMTP DATA state

func (*Protocol) STARTTLS added in v0.1.1

func (proto *Protocol) STARTTLS(args string) (reply *Reply)

STARTTLS creates a reply to a STARTTLS command

func (*Protocol) Start

func (proto *Protocol) Start() *Reply

Start begins an SMTP conversation with a 220 reply, placing the state machine in ESTABLISH state.

type Reply

type Reply struct {
	Status int

	Done func()
	// contains filtered or unexported fields

Reply is a struct representing an SMTP reply (status code + lines)

func ReplyAuthOk

func ReplyAuthOk() *Reply

ReplyAuthOk creates a 235 authentication successful reply

func ReplyAuthResponse

func ReplyAuthResponse(response string) *Reply

ReplyAuthResponse creates a 334 authentication reply

func ReplyBye

func ReplyBye() *Reply

ReplyBye creates a 221 Bye reply

func ReplyDataResponse

func ReplyDataResponse() *Reply

ReplyDataResponse creates a 354 data reply

func ReplyError

func ReplyError(err error) *Reply

ReplyError creates a 500 error reply

func ReplyIdent

func ReplyIdent(ident string) *Reply

ReplyIdent creates a 220 welcome reply

func ReplyInvalidAuth

func ReplyInvalidAuth() *Reply

ReplyInvalidAuth creates a 535 error reply

func ReplyLineTooLong added in v0.1.1

func ReplyLineTooLong() *Reply

ReplyLineTooLong creates a 500 Line too long reply

func ReplyMustIssueSTARTTLSFirst added in v0.1.1

func ReplyMustIssueSTARTTLSFirst() *Reply

ReplyMustIssueSTARTTLSFirst creates a 530 reply for RFC3207

func ReplyOk

func ReplyOk(message ...string) *Reply

ReplyOk creates a 250 Ok reply

func ReplyReadyToStartTLS added in v0.1.1

func ReplyReadyToStartTLS(callback func()) *Reply

ReplyReadyToStartTLS creates a 220 ready to start TLS reply

func ReplyRecipientOk

func ReplyRecipientOk(recipient string) *Reply

ReplyRecipientOk creates a 250 Sender ok reply

func ReplySenderOk

func ReplySenderOk(sender string) *Reply

ReplySenderOk creates a 250 Sender ok reply

func ReplyStorageFailed

func ReplyStorageFailed(reason string) *Reply

ReplyStorageFailed creates a 452 error reply

func ReplySyntaxError added in v0.1.1

func ReplySyntaxError(response string) *Reply

ReplySyntaxError creates a 501 Syntax error reply

func ReplyTooManyRecipients added in v0.1.1

func ReplyTooManyRecipients() *Reply

ReplyTooManyRecipients creates a 552 too many recipients reply

func ReplyUnrecognisedCommand

func ReplyUnrecognisedCommand() *Reply

ReplyUnrecognisedCommand creates a 500 Unrecognised command reply

func ReplyUnsupportedAuth

func ReplyUnsupportedAuth() *Reply

ReplyUnsupportedAuth creates a 504 unsupported authentication reply

func (Reply) Lines

func (r Reply) Lines() []string

Lines returns the formatted SMTP reply

type State

type State int

State represents the state of an SMTP conversation

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
t or T : Toggle theme light dark auto
y or Y : Canonical URL