smtpd

package module
v0.0.0-...-2e35f13 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2017 License: MIT Imports: 16 Imported by: 3

README

smtpd

Build Status

This package is trying to implement RFC-compatible SMTP server fo go.

Example

Requirements

  • swaks to test our SMTP server

Starting

See examples/.

Save this code to example.go(same code could be found in examples/logging-smtpd/main.go):

package main

import (
	"io/ioutil"
	"net"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/davecgh/go-spew/spew"

	"github.com/corpix/smtpd"
)

type smtpServer struct{}

func (s *smtpServer) ServeSMTP(c net.Conn, e *smtpd.Envelope) {
	logrus.Infof(
		"Received message from %s those envelope: %s\n",
		c.RemoteAddr(),
		spew.Sdump(e),
	)
	msg, err := e.Message()
	if err != nil {
		panic(err)
	}

	body, err := ioutil.ReadAll(msg.Body)
	if err != nil {
		panic(err)
	}
	logrus.Infof("Message body: %s\n", body)
}

func main() {
	var err error

	c, err := net.Listen("tcp", "127.0.0.1:2525")
	if err != nil {
		panic(err)
	}

	for {
		err = smtpd.Serve(c, &smtpServer{})
		if err != nil {
			logrus.Error(err)
			time.Sleep(1 * time.Second)
		}
	}
}

And run the server:

go run example.go

It will listen on 127.0.0.1:2525.

Send mail

swaks -f me@example.org -t test@example.com --server 127.0.0.1:2525

You should see output similar to this:

# remote 127.0.0.1:55912 at 2017-02-16 15:42:56 +0000
w 220 localhost go-smtpd
r EHLO localhost
w 250-localhost Hello 127.0.0.1:55912
w 250-8BITMIME
w 250-PIPELINING
w 250 HELP
r MAIL FROM:<me@example.org>
w 250 Okay, I'll believe you for now
r RCPT TO:<test@example.com>
w 250 Okay, I'll believe you for now
r DATA
w 354 Send away
r . <end of data>
w 250 I've put it in a can
r QUIT
w 221 Goodbye
# finished at 2017-02-16 15:42:56 +0000
INFO[0004] Received message from 127.0.0.1:55912 those envelope: (*smtpd.Envelope)(0xc4200d2000)({
 From: (string) (len=14) "me@example.org",
 To: ([]string) (len=1 cap=1) {
  (string) (len=16) "test@example.com"
 },
 Data: ([]uint8) (len=208 cap=208) {
  00000000  44 61 74 65 3a 20 54 68  75 2c 20 31 36 20 46 65  |Date: Thu, 16 Fe|
  00000010  62 20 32 30 31 37 20 31  35 3a 34 32 3a 35 36 20  |b 2017 15:42:56 |
  00000020  2b 30 30 30 30 0a 54 6f  3a 20 74 65 73 74 40 65  |+0000.To: test@e|
  00000030  78 61 6d 70 6c 65 2e 63  6f 6d 0a 46 72 6f 6d 3a  |xample.com.From:|
  00000040  20 6d 65 40 65 78 61 6d  70 6c 65 2e 6f 72 67 0a  | me@example.org.|
  00000050  53 75 62 6a 65 63 74 3a  20 74 65 73 74 20 54 68  |Subject: test Th|
  00000060  75 2c 20 31 36 20 46 65  62 20 32 30 31 37 20 31  |u, 16 Feb 2017 1|
  00000070  35 3a 34 32 3a 35 36 20  2b 30 30 30 30 0a 58 2d  |5:42:56 +0000.X-|
  00000080  4d 61 69 6c 65 72 3a 20  73 77 61 6b 73 20 76 32  |Mailer: swaks v2|
  00000090  30 31 33 30 32 30 39 2e  30 20 6a 65 74 6d 6f 72  |0130209.0 jetmor|
  000000a0  65 2e 6f 72 67 2f 6a 6f  68 6e 2f 63 6f 64 65 2f  |e.org/john/code/|
  000000b0  73 77 61 6b 73 2f 0a 0a  54 68 69 73 20 69 73 20  |swaks/..This is |
  000000c0  61 20 74 65 73 74 20 6d  61 69 6c 69 6e 67 0a 0a  |a test mailing..|
 }
})


INFO[0004] Message body: This is a test mailing

Credits

  • @siebenmann for awesome work on SMTPD package this packaged is based on.

License

MIT

Documentation

Index

Constants

View Source
const (
	Capability8BitMIME   = "250-8BITMIME"
	CapabilityPipelining = "250-PIPELINING"
	CapabilityStartTLS   = "250-STARTTLS"
	CapabilityAuth       = "250-AUTH"

	// https://www.ietf.org/rfc/rfc2821.txt (4.1.1.1)
	CapabilityHelp = "250 HELP" // should be last in EHLO sequence(SMTP uglyness)

	ReplyNoHelp                 = "214 No help here"
	ReplyReadyForTLS            = "220 Ready to start TLS"
	ReplyGoodbye                = "221 Goodbye"
	ReplyHelo                   = "250 %s Hello %v"
	ReplyEhlo                   = "250-%s Hello %v"
	ReplyAuthOk                 = "235 Authentication successful"
	ReplyCmdOk                  = "250 Okay"
	ReplyCmdOkForNow            = "250 Okay, I'll believe you for now"
	ReplyDataAccepted           = "250 I've put it in a can"
	ReplyDataAcceptedWithID     = "250 I've put it in a can called %s"
	ReplyAuthChallenge          = "334 %s"
	ReplyDataSendAway           = "354 Send away"
	ReplyServiceNotAvailable    = "421 Service not available now"
	ReplyMailboxNotAvailable    = "450 Not available"
	ReplyAuthTmpFail            = "454 Temporary authentication failure"
	ReplyInvalidAuthResp        = "501 Invalid authentication response"
	ReplyAuthAborted            = "501 Authentication aborted"
	ReplySyntaxError            = "501 Bad: %s"
	ReplyCmdNotSupported        = "502 Not supported"
	ReplyCmdOutOfSeq            = "503 Out of sequence command"
	ReplyCmdParamNotImplemented = "504 Command parameter not implemented"
	ReplyAuthRequired           = "530 Authentication required"
	ReplyRejected               = "550 Not accepted"
	ReplyBadAddr                = "550 Bad address"
	ReplyInvalidAuth            = "535 Authentication credentials invalid"
	ReplyTooManyBadCmds         = "554 Too many bad commands"
	ReplyDataRejectedWithID     = "554 Not put in a can called %s"

	ArgTooManyBadCmds = "too many bad commands"
)
View Source
const TimeFmt = "2006-01-02 15:04:05 -0700"

The time format we log messages in.

Variables

View Source
var (
	// ErrCmdContainsNonASCII indicates that received command line contains
	// non 7-bit ASCII characters.
	ErrCmdContainsNonASCII = e.New("command contains non 7-bit ASCII")

	// ErrCmdUnrecognized indicates that we failed to extract the actual
	// SMTP command name from the command line.
	// Probably reason is non-RFC compliant format of the command line.
	ErrCmdUnrecognized = e.New("unrecognized command")

	// ErrCmdHasNoArg indicates that we have found arguments while this
	// command should not receive any arguments.
	ErrCmdHasNoArg = e.New("SMTP command does not take an argument")

	// ErrCmdRequiresArg indicates that this command has one argument,
	// but we received this command without the argument.
	ErrCmdRequiresArg = e.New("SMTP command requires an argument")

	// ErrCmdRequiresArgs indicates that this command has one argument,
	// but we received this command without the argument.
	ErrCmdRequiresArgs = e.New("SMTP command requires an arguments")

	// ErrCmdRequiresAddress indicates that address is required for this command.
	ErrCmdRequiresAddress = e.New("SMTP command requires an address")

	// ErrCmdImpropperArgFmt indicates that command arguments has improper format.
	ErrCmdImpropperArgFmt = e.New("improper argument formatting")
)
View Source
var DefaultLimits = Limits{
	CmdInput:  2 * time.Minute,
	MsgInput:  10 * time.Minute,
	ReplyOut:  2 * time.Minute,
	TLSSetup:  4 * time.Minute,
	MsgSize:   5 * 1024 * 1024,
	CmdLine:   512,
	AuthInput: 12288,
	BadCmds:   5,
	NoParams:  true,
}

The default limits that are applied if you do not specify anything. Two minutes for command input and command replies, ten minutes for receiving messages, and 5 Mbytes of message size.

Note that these limits are not necessarily RFC compliant, although they should be enough for real email clients.

View Source
var DefaultServerConfig = &ServerConfig{
	PoolSize:       16,
	ProcessThreads: 4,
}

DefaultServerConfig is a default values for Server to start with.

View Source
var (
	// EnvelopeHashDelimiter is a delimiter that should be used
	// while calculating Hash.
	EnvelopeHashDelimiter = []byte{0}
)

Functions

func Serve

func Serve(l net.Listener, h Handler) error

Serve serves SMTP protocol with default configuration.

Types

type AuthConfig

type AuthConfig struct {
	// Both slices should contain uppercase SASL mechanism names,
	// e.g. PLAIN, LOGIN, EXTERNAL.
	Mechanisms    []string // supported mechanisms before STARTTLS.
	TLSMechanisms []string // supported mechanisms after STARTTLS.
}

AuthConfig specifies the authentication mechanisms that the server announces as supported.

type AuthFunc

type AuthFunc func(c *Conn, input []byte)

An AuthFunc implements one step of a SASL authentication dialog. The parameter input is the decoded SASL response from the client. Each time it's called, the function should either call Accept/Reject on the connection or send a challenge using AuthChallenge. The input parameter may be nil (the client sent absolutely nothing) or empty (the client sent a '=').

TODO: is a completely blank line an RFC error that should cause the authentication to fail and the connection to abort?

If an AuthFunc is called and does none of these, it is currently equivalent to calling .AuthChallenge(nil). However doing this is considered an error, not a guaranteed API, and may someday have other effects (eg aborting the authentication dialog).

type Command

type Command int

Command represents known SMTP commands in encoded form.

const (
	BadCmd Command = iota
	HELO
	EHLO
	MAILFROM
	RCPTTO
	DATA
	QUIT
	RSET
	NOOP
	VRFY
	EXPN
	HELP
	AUTH
	STARTTLS
)

Recognized SMTP commands. Not all of them do anything (e.g. VRFY and EXPN are just refused).

func (Command) String

func (v Command) String() string

type Config

type Config struct {
	TLSConfig *tls.Config   // TLS configuration if TLS is to be enabled.
	Limits    *Limits       // the limits applied to the connection.
	Auth      *AuthConfig   // if non-nil, client must authenticate before MAIL FROM.
	Delay     time.Duration // delay every character in replies by this much.
	SayTime   bool          // report the time and date in the server banner.
	LocalName string        // the local hostname to use in messages.
	SftName   string        // the software name to use in messages.
	Announce  string        // extra stuff to announce in greeting banner.
}

Config represents the configuration for a Conn. If unset, Limits is DefaultLimits, LocalName is 'localhost', and SftName is 'go-smtpd'.

type Conn

type Conn struct {
	Config Config // Connection configuration.

	TLSOn    bool                // TLS is on in this connection.
	TLSState tls.ConnectionState // TLS connection state.
	// contains filtered or unexported fields
}

Conn represents an ongoing SMTP connection. The TLS fields are read-only.

Note that this structure cannot be created by hand. Call NewConn.

Conn connections always advertise support for PIPELINING and 8BITMIME. STARTTLS is advertised if the Config passed to NewConn() has a non-nil TLSConfig. AUTH is advertised if the Config has a non-nil Auth.

Conn.Config can be altered to some degree after Conn is created in order to manipulate features on the fly. Note that Conn.Config.Limits is a pointer and so its fields should not be altered unless you know what you're doing and it's your Limits to start with.

func NewConn

func NewConn(conn net.Conn, cfg Config, log io.Writer) *Conn

NewConn creates a new SMTP conversation from conn, the underlying network connection involved. servername is the server name displayed in the greeting banner. A trace of SMTP commands and responses (but not email messages) will be written to log if it's non-nil.

Log messages start with a character, then a space, then the message. 'r' means read from network (client input), 'w' means written to the network (server replies), '!' means an error, and '#' is tracking information for the start or the end of the connection. Further information is up to whatever is behind 'log' to add.

func (*Conn) Accept

func (c *Conn) Accept()

Accept accepts the current SMTP command, ie gives an appropriate 2xx reply to the client.

func (*Conn) AcceptData

func (c *Conn) AcceptData(id string)

AcceptData accepts a message (ie, a post-DATA blob) with an ID that is reported to the client in the 2xx message. It only does anything when the Conn needs to reply to a DATA blob.

func (*Conn) AcceptMsg

func (c *Conn) AcceptMsg(format string, elems ...interface{})

AcceptMsg accepts MAIL FROM, RCPT TO, AUTH, DATA, or message bodies with the given fmt.Printf style message that you supply. The generated message may include embedded newlines for a multi-line reply. This cannot be applied to EHLO/HELO messages; if called for one of them, it is equivalent to Accept().

func (*Conn) AuthChallenge

func (c *Conn) AuthChallenge(data []byte)

AuthChallenge sends an authentication challenge to the client. It only works during authentication.

func (*Conn) Authenticate

func (c *Conn) Authenticate(mech AuthFunc) (success bool)

Authenticate executes a SASL authentication dialog with the client. The given function is invoked until it calls Accept/Reject or the client aborts the dialog (or an error happens).

Note that Authenticate() may return after a network error. In this case calling Next() will immediately return an ABORT event. As a corollary there is no guarantee that your AuthFunc will be called even once.

Using a nil AuthFunc is an error. Authenticate() generously doesn't panic on you and instead immediately rejects the authentication.

func (*Conn) Next

func (c *Conn) Next() EventInfo

Next returns the next high-level event from the SMTP connection.

Next() guarantees that the SMTP protocol ordering requirements are followed and only returns HELO/EHLO, AUTH, MAIL FROM, RCPT TO, and DATA commands, and the actual message submitted. The caller must reset all accumulated information about a message when it sees either EHLO/HELO or MAIL FROM.

For commands and GOTDATA, the caller may call Reject() or Tempfail() to reject or tempfail the command. Calling Accept() is optional; Next() will do it for you implicitly. It is invalid to call Next() after it has returned a DONE or ABORT event.

For the AUTH command, Next() will return a COMMAND event where Arg is set to the mechanism requested by the client. The mechanism is validated against the list of mechanisms provided in the config. The AUTH command event begins an authentication dialog, during which one or more AUTHRESP events are returned. The first AUTHRESP event contains the initial response from the AUTH command and may be empty. The dialog ends if an AUTHABORT or ABORT event is returned or when the AUTH command is accepted/rejected. Next will not accept the AUTH command automatically. If no reply is sent for an AUTHRESP event, the client receives an empty challenge. Under almost all situations you want to respond to a AUTH command not directly through calling .Next() but by calling .Authenticate() to handle the full details.

Next() does almost no checks on the value of EHLO/HELO, MAIL FROM, and RCPT TO. For MAIL FROM and RCPT TO it requires them to actually be present, but that's about it. It will accept blank EHLO/HELO (ie, no argument at all). It is up to the caller to do more validation and then call Reject() (or Tempfail()) as appropriate. MAIL FROM addresses may be blank (""), indicating the null sender ('<>'). RCPT TO addresses cannot be; Next() will fail those itself.

TLSERROR is returned if the client tried STARTTLS on a TLS-enabled connection but the TLS setup failed for some reason (eg the client only supports SSLv2). The caller can use this to, eg, decide not to offer TLS to that client in the future. No further activity can happen on a connection once TLSERROR is returned; the connection is considered dead and calling .Next() again will yield an ABORT event. The Arg of a TLSERROR event is the TLS error in string form.

func (*Conn) Reject

func (c *Conn) Reject()

Reject rejects the curent SMTP command, ie gives the client an appropriate 5xx message.

func (*Conn) RejectData

func (c *Conn) RejectData(id string)

RejectData rejects a message with an ID that is reported to the client in the 5xx message.

func (*Conn) RejectMsg

func (c *Conn) RejectMsg(format string, elems ...interface{})

RejectMsg rejects the current SMTP command with the fmt.Printf style message that you supply. The generated message may include embedded newlines for a multi-line reply.

func (*Conn) Tempfail

func (c *Conn) Tempfail()

Tempfail temporarily rejects the current SMTP command, ie it gives the client an appropriate 4xx reply. Properly implemented clients will retry temporary failures later.

func (*Conn) TempfailMsg

func (c *Conn) TempfailMsg(format string, elems ...interface{})

TempfailMsg temporarily rejects the current SMTP command with a 4xx code and the fmt.Printf style message that you supply. The generated message may include embedded newlines for a multi-line reply.

type Envelope

type Envelope struct {
	From string
	To   []string
	Data []byte
}

Envelope is a wrapper around general message fields and data.

func (*Envelope) Hash

func (e *Envelope) Hash() []byte

Hash calculates sha256 hash sum of the envelope contents.

func (*Envelope) HashString

func (e *Envelope) HashString() string

HashString is a Hash variant that encodes a result with hex.

func (*Envelope) Message

func (e *Envelope) Message() (*mail.Message, error)

Message decodes Envelope.Data with mail.ReadMessage() and returns the result.

type Event

type Event int

An Event is the sort of event that is returned by Conn.Next().

const (
	COMMAND   Event = iota
	AUTHRESP        // client sent SASL response
	AUTHABORT       // client aborted SASL dialog by sending '*'
	GOTDATA         // received DATA
	DONE            // client sent QUIT
	ABORT           // input or output error or timeout.
	TLSERROR        // error during TLS setup. Connection is dead.
)

The different types of SMTP events returned by Next().

type EventInfo

type EventInfo struct {
	What Event
	Cmd  Command
	Arg  string
}

EventInfo is what Conn.Next() returns to represent events. Cmd and Arg come from ParsedLine.

type Handler

type Handler interface {
	ServeSMTP(net.Conn, *Envelope)
}

Handler is a SMTP message(Envelope) handler.

type Limits

type Limits struct {
	CmdInput  time.Duration // client commands, eg MAIL FROM.
	MsgInput  time.Duration // total time to get the email message itself.
	ReplyOut  time.Duration // server replies to client commands.
	TLSSetup  time.Duration // time limit to finish STARTTLS TLS setup.
	MsgSize   int64         // total size of an email message.
	CmdLine   int64         // command line length https://www.ietf.org/rfc/rfc2821.txt (4.5.3.1).
	AuthInput int64         // auth response length https://www.ietf.org/rfc/rfc4954.txt (4).
	BadCmds   int           // how many unknown commands before abort.
	NoParams  bool          // reject MAIL FROM/RCPT TO with parameters.
}

Limits has the time(see time.Duration fields of a struct) and message limits for a Conn, as well as some additional options.

A Conn always accepts 'BODY=[7BIT|8BITMIME]' as the sole MAIL FROM parameter, since it advertises support for 8BITMIME.

type ParsedLine

type ParsedLine struct {
	Cmd Command
	Arg string
	// Params is K=V for ESMTP MAIL FROM and RCPT TO
	// or the initial SASL response for AUTH
	Params string
}

ParsedLine represents a parsed SMTP command line. Err is set if there was an error, empty otherwise. Cmd may be BadCmd or a command, even if there was an error.

See http://www.ietf.org/rfc/rfc1869.txt for the general discussion of params. We do not parse them.

func ParseCmd

func ParseCmd(line string) (*ParsedLine, error)

ParseCmd parses a SMTP command line and returns the result. The line should have the ending CRLF already removed.

type Server

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

Server describes a general data structures required to start SMTP server.

func New

func New(cfg *ServerConfig, handler Handler) (*Server, error)

New creates a new Server with specified ServerConfig(pass nil to use the default) and Handler.

func (*Server) Serve

func (s *Server) Serve(l net.Listener) error

Serve serves SMTP protocol for Listener.

type ServerConfig

type ServerConfig struct {
	PoolSize       int
	ProcessThreads int
}

ServerConfig is a configuration for Server.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL