Skip to content

Commit

Permalink
server: Immediately pass envelope information to the backend
Browse files Browse the repository at this point in the history
This allows backend to return an error on MAIL FROM or RCPT TO
(#22).

backendutil.TransformBackend is removed for reasons explained here:
#22 (comment)
  • Loading branch information
foxcpp authored and emersion committed Mar 22, 2019
1 parent ffb207c commit 1a1363a
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 122 deletions.
21 changes: 14 additions & 7 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ var (
type Backend interface {
// Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to
// support this.
Login(state *ConnectionState, username, password string) (User, error)
Login(state *ConnectionState, username, password string) (Session, error)

// Called if the client attempts to send mail without logging in first.
// Return smtp.ErrAuthRequired if you don't want to support this.
AnonymousLogin(state *ConnectionState) (User, error)
AnonymousLogin(state *ConnectionState) (Session, error)
}

// An authenticated user.
type User interface {
// Send an e-mail.
Send(from string, to []string, r io.Reader) error
// Logout is called when this User will no longer be used.
type Session interface {
// Discard currently processed message.
Reset()

// Free all resources associated with session.
Logout() error

// Set return path for currently processed message.
Mail(from string) error
// Add recipient for currently processed message.
Rcpt(to string) error
// Set currently processed message contents and send it.
Data(r io.Reader) error
}
2 changes: 0 additions & 2 deletions backendutil/backendutil.go

This file was deleted.

43 changes: 0 additions & 43 deletions backendutil/transform.go

This file was deleted.

8 changes: 0 additions & 8 deletions backendutil/transform_test.go

This file was deleted.

2 changes: 1 addition & 1 deletion client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ func TestHello(t *testing.T) {
}
}
case 9:
err = c.Noop()
err = c.Noop()
default:
t.Fatalf("Unhandled command")
}
Expand Down
88 changes: 50 additions & 38 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,6 @@ import (
"time"
)

// A SMTP message.
type message struct {
// The message contents.
io.Reader

// The sender e-mail address.
From string
// The recipients e-mail addresses.
To []string
}

type ConnectionState struct {
Hostname string
RemoteAddr net.Addr
Expand All @@ -36,10 +25,12 @@ type Conn struct {
text *textproto.Conn
server *Server
helo string
msg *message
nbrErrors int
user User
session Session
locker sync.Mutex

fromReceived bool
recipients []string
}

func newConn(c net.Conn, s *Server) *Conn {
Expand Down Expand Up @@ -134,23 +125,22 @@ func (c *Conn) Server() *Server {
return c.server
}

func (c *Conn) User() User {
func (c *Conn) Session() Session {
c.locker.Lock()
defer c.locker.Unlock()
return c.user
return c.session
}

// Setting the user resets any message beng generated
func (c *Conn) SetUser(user User) {
func (c *Conn) SetSession(session Session) {
c.locker.Lock()
defer c.locker.Unlock()
c.user = user
c.msg = &message{}
c.session = session
}

func (c *Conn) Close() error {
if user := c.User(); user != nil {
user.Logout()
if session := c.Session(); session != nil {
session.Logout()
}

return c.conn.Close()
Expand Down Expand Up @@ -234,15 +224,15 @@ func (c *Conn) handleMail(arg string) {
return
}

if c.User() == nil {
if c.Session() == nil {
state := c.State()
user, err := c.server.Backend.AnonymousLogin(&state)
session, err := c.server.Backend.AnonymousLogin(&state)
if err != nil {
c.WriteResponse(502, err.Error())
return
}

c.SetUser(user)
c.SetSession(session)
}

if len(arg) < 6 || strings.ToUpper(arg[0:5]) != "FROM:" {
Expand Down Expand Up @@ -285,13 +275,22 @@ func (c *Conn) handleMail(arg string) {
}
}

c.msg.From = from
if err := c.Session().Mail(from); err != nil {
if smtpErr, ok := err.(*SMTPError); ok {
c.WriteResponse(smtpErr.Code, smtpErr.Message)
return
}
c.WriteResponse(451, err.Error())
return
}

c.WriteResponse(250, fmt.Sprintf("Roger, accepting mail from <%v>", from))
c.fromReceived = true
}

// MAIL state -> waiting for RCPTs followed by DATA
func (c *Conn) handleRcpt(arg string) {
if c.msg == nil || c.msg.From == "" {
if !c.fromReceived {
c.WriteResponse(502, "Missing MAIL FROM command.")
return
}
Expand All @@ -304,12 +303,20 @@ func (c *Conn) handleRcpt(arg string) {
// TODO: This trim is probably too forgiving
recipient := strings.Trim(arg[3:], "<> ")

if c.server.MaxRecipients > 0 && len(c.msg.To) >= c.server.MaxRecipients {
if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients {
c.WriteResponse(552, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients))
return
}

c.msg.To = append(c.msg.To, recipient)
if err := c.Session().Rcpt(recipient); err != nil {
if smtpErr, ok := err.(*SMTPError); ok {
c.WriteResponse(smtpErr.Code, smtpErr.Message)
return
}
c.WriteResponse(451, err.Error())
return
}
c.recipients = append(c.recipients, recipient)
c.WriteResponse(250, fmt.Sprintf("I'll make sure <%v> gets this", recipient))
}

Expand Down Expand Up @@ -375,7 +382,7 @@ func (c *Conn) handleAuth(arg string) {
}
}

if c.User() != nil {
if c.Session() != nil {
c.WriteResponse(235, "Authentication succeeded")
}
}
Expand Down Expand Up @@ -415,7 +422,7 @@ func (c *Conn) handleData(arg string) {
return
}

if c.msg == nil || c.msg.From == "" || len(c.msg.To) == 0 {
if !c.fromReceived || len(c.recipients) == 0 {
c.WriteResponse(502, "Missing RCPT TO command.")
return
}
Expand All @@ -427,9 +434,9 @@ func (c *Conn) handleData(arg string) {
code int
msg string
)
c.msg.Reader = newDataReader(c)
err := c.User().Send(c.msg.From, c.msg.To, c.msg.Reader)
io.Copy(ioutil.Discard, c.msg.Reader) // Make sure all the data has been consumed
r := newDataReader(c)
err := c.Session().Data(r)
io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed
if err != nil {
if smtperr, ok := err.(*SMTPError); ok {
code = smtperr.Code
Expand All @@ -445,7 +452,7 @@ func (c *Conn) handleData(arg string) {

if c.server.LMTP {
// TODO: support per-recipient responses
for _, rcpt := range c.msg.To {
for _, rcpt := range c.recipients {
c.WriteResponse(code, "<"+rcpt+"> "+msg)
}
} else {
Expand Down Expand Up @@ -497,17 +504,22 @@ func (c *Conn) reset() {
c.locker.Lock()
defer c.locker.Unlock()

if c.user != nil {
c.user.Logout()
if c.session != nil {
c.session.Logout()
}

c.user = nil
c.msg = nil
c.session = nil
c.fromReceived = false
c.recipients = nil
}

func (c *Conn) resetMessage() {
c.locker.Lock()
defer c.locker.Unlock()

c.msg = &message{}
if c.session != nil {
c.session.Reset()
}
c.fromReceived = false
c.recipients = nil
}
4 changes: 2 additions & 2 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ func NewServer(be Backend) *Server {
}

state := conn.State()
user, err := be.Login(&state, username, password)
session, err := be.Login(&state, username, password)
if err != nil {
return err
}

conn.SetUser(user)
conn.SetSession(session)
return nil
})
},
Expand Down
54 changes: 33 additions & 21 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,53 +25,65 @@ type backend struct {
userErr error
}

func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.User, error) {
func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) {
if be.userErr != nil {
return &user{}, be.userErr
return &session{}, be.userErr
}

if username != "username" || password != "password" {
return nil, errors.New("Invalid username or password")
}
return &user{backend: be}, nil
return &session{backend: be}, nil
}

func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.User, error) {
func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) {
if be.userErr != nil {
return &user{}, be.userErr
return &session{}, be.userErr
}

return &user{backend: be, anonymous: true}, nil
return &session{backend: be, anonymous: true}, nil
}

type user struct {
type session struct {
backend *backend
anonymous bool

msg *message
}

func (s *session) Reset() {
s.msg = &message{}
}

func (s *session) Logout() error {
return nil
}

func (s *session) Mail(from string) error {
s.Reset()
s.msg.From = from
return nil
}

func (u *user) Send(from string, to []string, r io.Reader) error {
func (s *session) Rcpt(to string) error {
s.msg.To = append(s.msg.To, to)
return nil
}

func (s *session) Data(r io.Reader) error {
if b, err := ioutil.ReadAll(r); err != nil {
return err
} else {
msg := &message{
From: from,
To: to,
Data: b,
}

if u.anonymous {
u.backend.anonmsgs = append(u.backend.anonmsgs, msg)
s.msg.Data = b
if s.anonymous {
s.backend.anonmsgs = append(s.backend.anonmsgs, s.msg)
} else {
u.backend.messages = append(u.backend.messages, msg)
s.backend.messages = append(s.backend.messages, s.msg)
}
}
return nil
}

func (u *user) Logout() error {
return nil
}

type serverConfigureFunc func(*smtp.Server)

var (
Expand Down

0 comments on commit 1a1363a

Please sign in to comment.