Skip to content

Commit

Permalink
Merge branch 'master' into case-insensitive
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion committed Sep 17, 2018
2 parents bdf1d29 + eb6da5f commit 1bcda00
Show file tree
Hide file tree
Showing 7 changed files with 474 additions and 63 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ func (bkd *Backend) Login(username, password string) (smtp.User, error) {
return &User{}, nil
}

// Require clients to authenticate using SMTP AUTH before sending emails
func (bkd *Backend) AnonymousLogin() (smtp.User, error) {
return nil, smtp.ErrAuthRequired
}

type User struct{}

func (u *User) Send(from string, to []string, r io.Reader) error {
Expand Down
13 changes: 12 additions & 1 deletion backend.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
package smtp

import (
"errors"
"io"
)

var (
ErrAuthRequired = errors.New("Please authenticate first")
ErrAuthUnsupported = errors.New("Authentication not supported")
)

// A SMTP server backend.
type Backend interface {
// Authenticate a user.
// Authenticate a user. Return smtp.ErrAuthUnsupported if you don't want to
// support this.
Login(username, password string) (User, 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() (User, error)
}

// An authenticated user.
Expand Down
123 changes: 87 additions & 36 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net"
"net/textproto"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -63,6 +63,16 @@ func (c *Conn) init() {
c.text = textproto.NewConn(rwc)
}

func (c *Conn) unrecognizedCommand(cmd string) {
c.WriteResponse(500, fmt.Sprintf("Syntax error, %v command unrecognized", cmd))

c.nbrErrors++
if c.nbrErrors > 3 {
c.WriteResponse(500, "Too many unrecognized commands")
c.Close()
}
}

// Commands are dispatched to the appropriate handler functions.
func (c *Conn) handle(cmd string, arg string) {
if cmd == "" {
Expand All @@ -74,8 +84,16 @@ func (c *Conn) handle(cmd string, arg string) {
case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN":
// These commands are not implemented in any state
c.WriteResponse(502, fmt.Sprintf("%v command not implemented", cmd))
case "HELO", "EHLO":
c.handleGreet((cmd == "EHLO"), arg)
case "HELO", "EHLO", "LHLO":
lmtp := cmd == "LHLO"
enhanced := lmtp || cmd == "EHLO"
if c.server.LMTP && !lmtp {
c.WriteResponse(500, "This is a LMTP server, use LHLO")
}
if !c.server.LMTP && lmtp {
c.WriteResponse(500, "This is not a LMTP server")
}
c.handleGreet(enhanced, arg)
case "MAIL":
c.handleMail(arg)
case "RCPT":
Expand All @@ -93,17 +111,15 @@ func (c *Conn) handle(cmd string, arg string) {
c.WriteResponse(221, "Goodnight and good luck")
c.Close()
case "AUTH":
c.handleAuth(arg)
if c.server.AuthDisabled {
c.unrecognizedCommand(cmd)
} else {
c.handleAuth(arg)
}
case "STARTTLS":
c.handleStartTLS()
default:
c.WriteResponse(500, fmt.Sprintf("Syntax error, %v command unrecognized", cmd))

c.nbrErrors++
if c.nbrErrors > 3 {
c.WriteResponse(500, "Too many unrecognized commands")
c.Close()
}
c.unrecognizedCommand(cmd)
}
}

Expand All @@ -117,10 +133,12 @@ func (c *Conn) User() User {
return c.user
}

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

func (c *Conn) Close() error {
Expand All @@ -137,6 +155,11 @@ func (c *Conn) IsTLS() bool {
return ok
}

func (c *Conn) authAllowed() bool {
return !c.server.AuthDisabled &&
(c.IsTLS() || c.server.AllowInsecureAuth)
}

// GREET state -> waiting for HELO
func (c *Conn) handleGreet(enhanced bool, arg string) {
if !enhanced {
Expand All @@ -162,7 +185,7 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
if c.server.TLSConfig != nil && !c.IsTLS() {
caps = append(caps, "STARTTLS")
}
if c.IsTLS() || c.server.AllowInsecureAuth {
if c.authAllowed() {
authCap := "AUTH"
for name, _ := range c.server.auths {
authCap += " " + name
Expand All @@ -186,26 +209,38 @@ func (c *Conn) handleMail(arg string) {
c.WriteResponse(502, "Please introduce yourself first.")
return
}
if c.msg == nil {
c.WriteResponse(502, "Please authenticate first.")
return

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

c.SetUser(user)
}

// Match FROM, while accepting '>' as quoted pair and in double quoted strings
// (?i) makes the regex case insensitive, (?:) is non-grouping sub-match
re := regexp.MustCompile("(?i)^FROM:\\s*<((?:\\\\>|[^>])+|\"[^\"]+\"@[^>]+)>( [\\w= ]+)?$")
m := re.FindStringSubmatch(arg)
if m == nil {
if len(arg) < 6 || strings.ToUpper(arg[0:5]) != "FROM:" {
c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:<address>")
return
}
fromArgs := strings.Split(strings.Trim(arg[5:], " "), " ")
if c.server.Strict {
if !strings.HasPrefix(fromArgs[0], "<") || !strings.HasSuffix(fromArgs[0], ">") {
c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:<address>")
return
}
}
from := strings.Trim(fromArgs[0], "<> ")
if from == "" {
c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:<address>")
return
}

from := m[1]

// This is where the Conn may put BODY=8BITMIME, but we already
// read the DATA as bytes, so it does not effect our processing.
if m[2] != "" {
args, err := parseArgs(m[2])
if len(fromArgs) > 1 {
args, err := parseArgs(fromArgs[1:])
if err != nil {
c.WriteResponse(501, "Unable to parse MAIL ESMTP parameters")
return
Expand Down Expand Up @@ -315,10 +350,8 @@ func (c *Conn) handleAuth(arg string) {
}
}

if c.User != nil {
if c.User() != nil {
c.WriteResponse(235, "Authentication succeeded")

c.msg = &message{}
}
}

Expand Down Expand Up @@ -365,15 +398,33 @@ func (c *Conn) handleData(arg string) {
// We have recipients, go to accept data
c.WriteResponse(354, "Go ahead. End your data with <CR><LF>.<CR><LF>")

var (
code int
msg string
)
c.msg.Reader = newDataReader(c)
if err := c.User().Send(c.msg.From, c.msg.To, c.msg.Reader); err != nil {
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
if err != nil {
if smtperr, ok := err.(*smtpError); ok {
c.WriteResponse(smtperr.Code, smtperr.Message)
code = smtperr.Code
msg = smtperr.Message
} else {
c.WriteResponse(554, "Error: transaction failed, blame it on the weather: "+err.Error())
code = 554
msg = "Error: transaction failed, blame it on the weather: " + err.Error()
}
} else {
code = 250
msg = "OK: queued"
}

if c.server.LMTP {
// TODO: support per-recipient responses
for _, rcpt := range c.msg.To {
c.WriteResponse(code, "<" + rcpt + "> " + msg)
}
} else {
c.WriteResponse(250, "Ok: queued")
c.WriteResponse(code, msg)
}

c.reset()
Expand Down Expand Up @@ -418,13 +469,13 @@ func (c *Conn) ReadLine() (string, error) {
}

func (c *Conn) reset() {
if user := c.User(); user != nil {
user.Logout()
c.locker.Lock()
defer c.locker.Unlock()

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

c.locker.Lock()
c.helo = ""
c.user = nil
c.msg = nil
c.locker.Unlock()
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/emersion/go-smtp

require github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197
24 changes: 12 additions & 12 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package smtp

import (
"fmt"
"regexp"
"strings"
)

Expand Down Expand Up @@ -40,18 +39,19 @@ func parseCmd(line string) (cmd string, arg string, err error) {
// string:
// " BODY=8BITMIME SIZE=1024"
// The leading space is mandatory.
func parseArgs(arg string) (args map[string]string, err error) {
args = map[string]string{}
re := regexp.MustCompile(" (\\w+)=(\\w+)")
pm := re.FindAllStringSubmatch(arg, -1)
if pm == nil {
return nil, fmt.Errorf("Failed to parse arg string: %q")
func parseArgs(args []string) (map[string]string, error) {
argMap := map[string]string{}
for _, arg := range args {
if arg == "" {
continue
}
m := strings.Split(arg, "=")
if len(m) != 2 {
return nil, fmt.Errorf("Failed to parse arg string: %q", arg)
}
argMap[strings.ToUpper(m[0])] = m[1]
}

for _, m := range pm {
args[strings.ToUpper(m[1])] = m[2]
}
return args, nil
return argMap, nil
}

func parseHelloArgument(arg string) (string, error) {
Expand Down
29 changes: 29 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/emersion/go-sasl"
)

var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket")

// A function that creates SASL servers.
type SaslServerFactory func(conn *Conn) sasl.Server

Expand All @@ -19,14 +21,22 @@ type Server struct {
Addr string
// The server TLS configuration.
TLSConfig *tls.Config
// Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a
// TCP listener.
LMTP bool

Domain string
MaxRecipients int
MaxIdleSeconds int
MaxMessageBytes int
AllowInsecureAuth bool
Strict bool
Debug io.Writer

// If set, the AUTH command will not be advertised and authentication
// attempts will be rejected. This setting overrides AllowInsecureAuth.
AuthDisabled bool

// The server backend.
Backend Backend

Expand Down Expand Up @@ -126,6 +136,10 @@ func (s *Server) handleConn(c *Conn) error {
//
// If s.Addr is blank, ":smtp" is used.
func (s *Server) ListenAndServe() error {
if s.LMTP {
return errTCPAndLMTP
}

addr := s.Addr
if addr == "" {
addr = ":smtp"
Expand All @@ -144,6 +158,10 @@ func (s *Server) ListenAndServe() error {
//
// If s.Addr is blank, ":smtps" is used.
func (s *Server) ListenAndServeTLS() error {
if s.LMTP {
return errTCPAndLMTP
}

addr := s.Addr
if addr == "" {
addr = ":smtps"
Expand All @@ -157,6 +175,17 @@ func (s *Server) ListenAndServeTLS() error {
return s.Serve(l)
}

// ListenAndServeUnix listens on a Unix address and then calls Serve to handle
// requests on incoming connections.
func (s *Server) ListenAndServeUnix(addr *net.UnixAddr) error {
l, err := net.ListenUnix("unix", addr)
if err != nil {
return err
}

return s.Serve(l)
}

// Close stops the server.
func (s *Server) Close() {
s.listener.Close()
Expand Down
Loading

0 comments on commit 1bcda00

Please sign in to comment.