smtp

package
v2.0.0-...-d85afef Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2025 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const (
	SMTP_CRLF                     string = "\r\n"
	SMTP_DATA_TERMINATOR          string = "\r\n.\r\n"
	SMTP_WELCOME_MESSAGE          string = "220 Welcome to MailSlurper!"
	SMTP_CLOSING_MESSAGE          string = "221 Bye"
	SMTP_OK_MESSAGE               string = "250 Ok"
	SMTP_DATA_RESPONSE_MESSAGE    string = "354 End data with <CR><LF>.<CR><LF>"
	SMTP_HELLO_RESPONSE_MESSAGE   string = "250 Hello. How very nice to meet you!"
	SMTP_ERROR_TRANSACTION_FAILED string = "554 Transaction failed"
)

Responses that are sent to SMTP clients.

View Source
const (
	SMTP_WORKER_IDLE    SMTPWorkerState = 0
	SMTP_WORKER_WORKING SMTPWorkerState = 1
	SMTP_WORKER_DONE    SMTPWorkerState = 100
	SMTP_WORKER_ERROR   SMTPWorkerState = 101

	RECEIVE_BUFFER_LEN         = 1024
	CONNECTION_TIMEOUT_MINUTES = 10
	COMMAND_TIMEOUT_SECONDS    = 5
)

Variables

View Source
var Commands = map[string]Command{
	"helo":      HELO,
	"ehlo":      HELO,
	"rcpt to":   RCPT,
	"mail from": MAIL,
	"send":      MAIL,
	"rset":      RSET,
	"quit":      QUIT,
	"data":      DATA,
	"noop":      NOOP,
}

Commands is a map of SMTP command strings to their int representation. This is primarily used because there can be more than one command to do the same things. For example, a client can send "helo" or "ehlo" to initiate the handshake.

View Source
var CommandsToStrings = map[Command]string{
	HELO: "HELO",
	RCPT: "RCPT TO",
	MAIL: "SEND",
	RSET: "RSET",
	QUIT: "QUIT",
	DATA: "DATA",
	NOOP: "NOOP",
}

CommandsToStrings is a friendly string representations of commands. Useful in error reporting.

View Source
var (
	ErrServerClosed = errors.New("server closed")
)

Functions

func GetCommandValue

func GetCommandValue(streamInput, command, delimiter string) (string, error)

GetCommandValue splits an input by colon (:) and returns the right hand side. If there isn't a split, or a missing colon, an InvalidCommandFormatError is returned.

func IsValidCommand

func IsValidCommand(streamInput, expectedCommand string) error

IsValidCommand returns an error if the input stream does not contain the expected command. The input and expected commands are lower cased, as we do not care about case when comparing.

Types

type Command

type Command int

Command represents a command issued over a TCP connection.

const (
	NONE Command = iota
	RCPT Command = iota
	MAIL Command = iota
	HELO Command = iota
	RSET Command = iota
	DATA Command = iota
	QUIT Command = iota
	NOOP Command = iota
)

func GetCommandFromString

func GetCommandFromString(input string) (Command, error)

GetCommandFromString takes a string and returns the integer command representation. For example if the string contains "DATA" then the value 1 (the constant DATA) will be returned.

func (Command) String

func (smtpCommand Command) String() string

String returns the string representation of a command.

type ConnectionExistsError

type ConnectionExistsError struct {
	Address string
}

An ConnectionExistsError is used to alert a client that there is already a connection by this address cached

func ConnectionExists

func ConnectionExists(address string) *ConnectionExistsError

ConnectionExists returns a new error object

func (*ConnectionExistsError) Error

func (err *ConnectionExistsError) Error() string

type ConnectionManager

type ConnectionManager struct {
	// contains filtered or unexported fields
}

ConnectionManager is responsible for maintaining, closing, and cleaning client connections. For every connection there is a worker. After an idle timeout period the manager will forceably close a client connection.

func NewConnectionManager

func NewConnectionManager(
	logger *slog.Logger,
	config *slurperio.Config,
	chStop chan struct{},
	mailItemChannel chan *model.MailItem,
	serverPool *ServerPool,
) *ConnectionManager

NewConnectionManager creates a new struct.

func (*ConnectionManager) Close

func (m *ConnectionManager) Close(connection net.Conn) error

Close will close a client connection. The state of the worker is only used for logging purposes.

func (*ConnectionManager) New

func (m *ConnectionManager) New(connection net.Conn) error

New attempts to track a new client connection. The SMTPListener will use this to track a client connection and its worker.

type ConnectionNotExistsError

type ConnectionNotExistsError struct {
	Address string
}

An ConnectionNotExistsError is used to alert a client that the specified connection is not in the ConnectionManager pool

func ConnectionNotExists

func ConnectionNotExists(address string) *ConnectionNotExistsError

ConnectionNotExists returns a new error object

func (*ConnectionNotExistsError) Error

func (err *ConnectionNotExistsError) Error() string

type ConnectionPool

type ConnectionPool map[string]*ConnectionPoolItem

ConnectionPool is a map of remote address to TCP connections and their workers.

func NewConnectionPool

func NewConnectionPool() ConnectionPool

NewConnectionPool creates a new empty map.

type ConnectionPoolItem

type ConnectionPoolItem struct {
	Connection net.Conn
	Worker     *Worker
}

ConnectionPoolItem is a single item in the pool. This tracks a connection to its worker.

func NewConnectionPoolItem

func NewConnectionPoolItem(connection net.Conn, worker *Worker) *ConnectionPoolItem

NewConnectionPoolItem create a new object.

type DataCommandExecutor

type DataCommandExecutor struct {
	// contains filtered or unexported fields
}

DataCommandExecutor process the Data TO command.

func NewDataCommandExecutor

func NewDataCommandExecutor(
	logger *slog.Logger,
	reader *Reader,
	writer *Writer,
	emailValidationService mailslurper.EmailValidationProvider,
	xssService sanitizer.IXSSServiceProvider,
) *DataCommandExecutor

NewDataCommandExecutor creates a new struct.

func (*DataCommandExecutor) Process

func (e *DataCommandExecutor) Process(streamInput string, mailItem *model.MailItem) error

Process processes the DATA command (constant DATA). When a client sends the DATA command there are three parts to the transmission content. Before this data can be processed this function will tell the client how to terminate the DATA block. We are asking clients to terminate with "\r\n.\r\n".

The first part is a set of header lines. Each header line is a header key (name), followed by a colon, followed by the value for that header key. For example a header key might be "Subject" with a value of "Testing Mail!".

After the header section there should be two sets of carriage return/line feed characters. This signals the end of the header block and the start of the message body.

Finally when the client sends the "\r\n.\r\n" the DATA transmission portion is complete. This function will return the following items.

1. Headers (MailHeader) 2. Body breakdown (MailBody) 3. error structure

type HelloCommandExecutor

type HelloCommandExecutor struct {
	// contains filtered or unexported fields
}

HelloCommandExecutor process the commands EHLO, HELO.

func NewHelloCommandExecutor

func NewHelloCommandExecutor(logger *slog.Logger, reader *Reader, writer *Writer) *HelloCommandExecutor

NewHelloCommandExecutor creates a new struct.

func (*HelloCommandExecutor) Process

func (e *HelloCommandExecutor) Process(streamInput string, mailItem *model.MailItem) error

Process handles the HELO greeting command.

type InvalidCommandError

type InvalidCommandError struct {
	InvalidCommand string
}

An InvalidCommandError is used to alert a client that the command passed in is invalid.

func InvalidCommand

func InvalidCommand(command string) *InvalidCommandError

InvalidCommand returns a new error object

func (*InvalidCommandError) Error

func (err *InvalidCommandError) Error() string

type InvalidCommandFormatError

type InvalidCommandFormatError struct {
	InvalidCommand string
}

An InvalidCommandFormatError is used to alert a client that the command passed in has an invalid format

func InvalidCommandFormat

func InvalidCommandFormat(command string) *InvalidCommandFormatError

InvalidCommandFormat returns a new error object

func (*InvalidCommandFormatError) Error

func (err *InvalidCommandFormatError) Error() string

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener sets up a server that listens on a TCP socket for connections. When a connection is received a worker is created to handle processing the mail on this connection.

func NewListener

func NewListener(
	logger *slog.Logger,
	config slurperio.ListenConfig,
	mailItemChannel chan *model.MailItem,
	serverPool *ServerPool,
	receivers []mailslurper.IMailItemReceiver,
	connectionManager mailslurper.IConnectionManager,
) (*Listener, error)

NewListener creates an Listener struct.

func (*Listener) Addr

func (l *Listener) Addr() net.Addr

func (*Listener) Close

func (l *Listener) Close() error

func (*Listener) Dispatch deprecated

func (l *Listener) Dispatch(ctx context.Context)

Deprecated:

Dispatch starts the process of handling SMTP client connections. The first order of business is to setup a channel for writing parsed mails, in the form of MailItemStruct variables, to our database. A goroutine is setup to listen on that channel and handles storage.

Meanwhile this method will loop forever and wait for client connections (blocking). When a connection is recieved a goroutine is started to create a new MailItemStruct and parser and the parser process is started. If the parsing is successful the MailItemStruct is added to a channel. An receivers passed in will be listening on that channel and may do with the mail item as they wish.

func (*Listener) ListenAndServe

func (l *Listener) ListenAndServe() error

ListenAndServe starts the process of handling SMTP client connections. The first order of business is to setup a channel for writing parsed mails, in the form of MailItemStruct variables, to our database. A goroutine is setup to listen on that channel and handles storage.

Meanwhile this method will loop forever and wait for client connections (blocking). When a connection is recieved a goroutine is started to create a new MailItemStruct and parser and the parser process is started. If the parsing is successful the MailItemStruct is added to a channel. An receivers passed in will be listening on that channel and may do with the mail item as they wish.

ListenAndServe always returns a non-nil error.

func (*Listener) Shutdown

func (l *Listener) Shutdown(_ context.Context) error

func (*Listener) Start deprecated

func (l *Listener) Start() error

Deprecated:

Start establishes a listening connection to a socket on an address.

type MailCommandExecutor

type MailCommandExecutor struct {
	// contains filtered or unexported fields
}

MailCommandExecutor process the MAIL FROM.

func NewMailCommandExecutor

func NewMailCommandExecutor(
	logger *slog.Logger,
	reader *Reader,
	writer *Writer,
	emailValidationService mailslurper.EmailValidationProvider,
	xssService sanitizer.IXSSServiceProvider,
) *MailCommandExecutor

NewMailCommandExecutor creates a new struct.

func (*MailCommandExecutor) Process

func (e *MailCommandExecutor) Process(streamInput string, mailItem *model.MailItem) error

Process handles the MAIL FROM command. This command tells us who the sender is.

type NoWorkerAvailableError

type NoWorkerAvailableError struct{}

NoWorkerAvailableError is an error used when no worker is available to service a SMTP connection request.

func NoWorkerAvailable

func NoWorkerAvailable() NoWorkerAvailableError

NoWorkerAvailable returns a new instance of the No Worker Available error

func (NoWorkerAvailableError) Error

func (err NoWorkerAvailableError) Error() string

type NoopCommandExecutor

type NoopCommandExecutor struct {
	// contains filtered or unexported fields
}

NoopCommandExecutor process the command NOOP.

func NewNoopCommandExecutor

func NewNoopCommandExecutor(logger *slog.Logger, writer *Writer) *NoopCommandExecutor

NewNoopCommandExecutor creates a new struct.

func (*NoopCommandExecutor) Process

func (e *NoopCommandExecutor) Process(streamInput string, mailItem *model.MailItem) error

Process handles the NOOP command.

type RcptCommandExecutor

type RcptCommandExecutor struct {
	// contains filtered or unexported fields
}

RcptCommandExecutor process the RCPT TO command.

func NewRcptCommandExecutor

func NewRcptCommandExecutor(
	logger *slog.Logger,
	reader *Reader,
	writer *Writer,
	emailValidationService mailslurper.EmailValidationProvider,
	xssService sanitizer.IXSSServiceProvider,
) *RcptCommandExecutor

NewRcptCommandExecutor creates a new struct.

func (*RcptCommandExecutor) Process

func (e *RcptCommandExecutor) Process(streamInput string, mailItem *model.MailItem) error

Process handles the RCPT TO command. This command tells us who the recipient is.

type Reader

type Reader struct {
	Connection net.Conn
	// contains filtered or unexported fields
}

Reader is a simple object for reading commands and responses from a connected TCP client.

func (*Reader) Read

func (r *Reader) Read() (string, error)

Read reads the raw data from the socket connection to our client. This will read on the socket until there is nothing left to read and an error is generated. This method blocks the socket for the number of milliseconds defined in CONN_TIMEOUT_MILLISECONDS. It then records what has been read in that time, then blocks again until there is nothing left on the socket to read. The final value is stored and returned as a string.

func (*Reader) ReadDataBlock

func (r *Reader) ReadDataBlock() (string, error)

ReadDataBlock is used by the SMTP DATA command. It will read data from the connection until the terminator is sent.

type ResetCommandExecutor

type ResetCommandExecutor struct {
	// contains filtered or unexported fields
}

ResetCommandExecutor process the command RSET.

func NewResetCommandExecutor

func NewResetCommandExecutor(logger *slog.Logger, writer *Writer) *ResetCommandExecutor

NewResetCommandExecutor creates a new struct.

func (*ResetCommandExecutor) Process

func (e *ResetCommandExecutor) Process(streamInput string, mailItem *model.MailItem) error

Process handles the RSET command.

type SMTPWorkerState

type SMTPWorkerState int

SMTPWorkerState defines states that a worker may be in. Typically a worker starts IDLE, the moves to WORKING, finally going to either DONE or ERROR.

type ServerPool

type ServerPool struct {
	// contains filtered or unexported fields
}

ServerPool represents a pool of SMTP workers. This will manage how many workers may respond to SMTP client requests and allocation of those workers.

func NewServerPool

func NewServerPool(maxWorkers int, xss sanitizer.IXSSServiceProvider, logger *slog.Logger) *ServerPool

NewServerPool creates a new server pool with a maximum number of SMTP workers. An array of workers is initialized with an ID and an initial state of SMTP_WORKER_IDLE.

func (*ServerPool) JoinQueue

func (pool *ServerPool) JoinQueue(worker *Worker)

JoinQueue adds a worker to the queue.

func (*ServerPool) NextWorker

func (pool *ServerPool) NextWorker(
	connection net.Conn,
	receiver chan *model.MailItem,
	chStop chan struct{},
	connectionCloseChannel chan net.Conn,
) (*Worker, error)

NextWorker retrieves the next available worker from the queue.

type Worker

type Worker struct {
	Connection             net.Conn
	EmailValidationService mailslurper.EmailValidationProvider
	Error                  error
	Reader                 *Reader
	Receiver               chan *model.MailItem
	State                  SMTPWorkerState
	WorkerID               int
	Writer                 *Writer
	XSSService             sanitizer.IXSSServiceProvider
	// contains filtered or unexported fields
}

Worker is responsible for executing, parsing, and processing a single TCP connection's email.

func NewWorker

func NewWorker(
	workerID int,
	pool *ServerPool,
	emailValidationService mailslurper.EmailValidationProvider,
	xssService sanitizer.IXSSServiceProvider,
	logger *slog.Logger,
) *Worker

NewWorker creates a new SMTP worker. An SMTP worker is responsible for parsing and working with SMTP mail data.

func (*Worker) Prepare

func (w *Worker) Prepare(
	connection net.Conn,
	receiver chan *model.MailItem,
	reader *Reader,
	writer *Writer,
	chStop chan struct{},
	connectionCloseChannel chan net.Conn,
)

Prepare tells a worker about the TCP connection they will work with, the IO handlers, and sets their state.

func (*Worker) TimeoutHasExpired

func (w *Worker) TimeoutHasExpired(startTime time.Time) bool

TimeoutHasExpired determines if the time elapsed since a start time has exceeded the command timeout.

func (*Worker) Work

func (w *Worker) Work()

Work is the function called by the SmtpListener when a client request is received. This will start the process by responding to the client, start processing commands, and finally close the connection.

type Writer

type Writer struct {
	Connection net.Conn
	// contains filtered or unexported fields
}

Writer is a simple object for writing commands and responses to a client connected on a TCP socket.

func (*Writer) SayGoodbye

func (w *Writer) SayGoodbye() error

SayGoodbye tells a client that we are done communicating. This sends a 221 response. It returns true/false for success and a string with any response.

func (*Writer) SayHello

func (w *Writer) SayHello() error

SayHello sends a hello message to a new client. The SMTP protocol dictates that you must be polite. :)

func (*Writer) SendDataResponse

func (w *Writer) SendDataResponse() error

SendDataResponse is a function to send a DATA response message.

func (*Writer) SendHELOResponse

func (w *Writer) SendHELOResponse() error

SendHELOResponse sends a HELO message to a client.

func (*Writer) SendOkResponse

func (w *Writer) SendOkResponse() error

SendOkResponse sends an OK to a client.

func (*Writer) SendResponse

func (w *Writer) SendResponse(response string) error

SendResponse sends a response to a client connection. It returns true/false for success and a string with any response.