Implemented MaxConnections, corrected examples, removed MailAddress.

This commit is contained in:
Christian Joergensen 2014-07-15 10:07:58 +02:00
parent c214cd0c15
commit 0e0eb0eda4
5 changed files with 84 additions and 27 deletions

View file

@ -5,12 +5,9 @@ import (
"strings" "strings"
) )
// MailAddress holds an e-mail address func parseAddress(src string) (string, error) {
type MailAddress string
func parseMailAddress(src string) (MailAddress, error) {
if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 { if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 {
return MailAddress(""), fmt.Errorf("Ill-formatted e-mail address: %s", src) return "", fmt.Errorf("Ill-formatted e-mail address: %s", src)
} }
return MailAddress(src[1 : len(src)-1]), nil return src[1 : len(src)-1], nil
} }

View file

@ -1,32 +1,32 @@
package smtpd package smtpd
import ( import (
"bitbucket.org/chrj/smtpd"
"errors" "errors"
"net"
"net/smtp" "net/smtp"
"strings" "strings"
) )
func ExampleServer() { func ExampleServer() {
var server *Server
// No-op server. Accepts and discards // No-op server. Accepts and discards
server := &smtpd.Server{} server = &Server{}
server.serve() server.ListenAndServe()
// Relay server. Accepts only from single IP address and forwards using the Gmail smtp // Relay server. Accepts only from single IP address and forwards using the Gmail smtp
server := &smtpd.Server{ server = &Server{
Addr: "0.0.0.0:10025", Addr: "0.0.0.0:10025",
HeloChecker: func(peer smtpd.Peer) error { HeloChecker: func(peer Peer) error {
if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") { if !strings.HasPrefix(peer.Addr.String(), "42.42.42.42:") {
return errors.New("Denied") return errors.New("Denied")
} }
return nil return nil
}, },
Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { Handler: func(peer Peer, env Envelope) error {
return smtp.SendMail( return smtp.SendMail(
"smtp.gmail.com:587", "smtp.gmail.com:587",
smtp.PlainAuth( smtp.PlainAuth(
@ -35,14 +35,14 @@ func ExampleServer() {
"password", "password",
"smtp.gmail.com", "smtp.gmail.com",
), ),
env.Sender, string(env.Sender),
env.Recipients, []string(env.Recipients),
env.Data, env.Data,
) )
}, },
} }
server.serve() server.ListenAndServe()
} }

View file

@ -148,7 +148,7 @@ func (session *session) handleMAIL(cmd command) {
return return
} }
addr, err := parseMailAddress(cmd.params[1]) addr, err := parseAddress(cmd.params[1])
if err != nil { if err != nil {
session.reply(502, "Ill-formatted e-mail address") session.reply(502, "Ill-formatted e-mail address")
@ -178,7 +178,7 @@ func (session *session) handleRCPT(cmd command) {
return return
} }
addr, err := parseMailAddress(cmd.params[1]) addr, err := parseAddress(cmd.params[1])
if err != nil { if err != nil {
session.reply(502, "Ill-formatted e-mail address") session.reply(502, "Ill-formatted e-mail address")

View file

@ -19,6 +19,9 @@ type Server struct {
ReadTimeout time.Duration // Socket timeout for read operations (default: 60s) ReadTimeout time.Duration // Socket timeout for read operations (default: 60s)
WriteTimeout time.Duration // Socket timeout for write operations (default: 60s) WriteTimeout time.Duration // Socket timeout for write operations (default: 60s)
MaxMessageSize int // Max message size in bytes (default: 10240000)
MaxConnections int // Max concurrent connections, use -1 to disable (default: 100)
// New e-mails are handed off to this function. // New e-mails are handed off to this function.
// Can be left empty for a NOOP server. // Can be left empty for a NOOP server.
// If an error is returned, it will be reported in the SMTP session. // If an error is returned, it will be reported in the SMTP session.
@ -28,8 +31,8 @@ type Server struct {
// Can be left empty for no restrictions. // Can be left empty for no restrictions.
// If an error is returned, it will be reported in the SMTP session. // If an error is returned, it will be reported in the SMTP session.
HeloChecker func(peer Peer) error // Called after HELO/EHLO. HeloChecker func(peer Peer) error // Called after HELO/EHLO.
SenderChecker func(peer Peer, addr MailAddress) error // Called after MAIL FROM. SenderChecker func(peer Peer, addr string) error // Called after MAIL FROM.
RecipientChecker func(peer Peer, addr MailAddress) error // Called after each RCPT TO. RecipientChecker func(peer Peer, addr string) error // Called after each RCPT TO.
// Enable PLAIN/LOGIN authentication, only available after STARTTLS. // Enable PLAIN/LOGIN authentication, only available after STARTTLS.
// Can be left empty for no authentication support. // Can be left empty for no authentication support.
@ -37,8 +40,6 @@ type Server struct {
TLSConfig *tls.Config // Enable STARTTLS support TLSConfig *tls.Config // Enable STARTTLS support
ForceTLS bool // Force STARTTLS usage ForceTLS bool // Force STARTTLS usage
MaxMessageSize int // Max message size in bytes (default: 10240000)
} }
// Peer represents the client connecting to the server // Peer represents the client connecting to the server
@ -51,8 +52,8 @@ type Peer struct {
// Envelope holds a message // Envelope holds a message
type Envelope struct { type Envelope struct {
Sender MailAddress Sender string
Recipients []MailAddress Recipients []string
Data []byte Data []byte
} }
@ -107,6 +108,14 @@ func (srv *Server) Serve(l net.Listener) error {
defer l.Close() defer l.Close()
var limiter chan struct{}
if srv.MaxConnections > 0 {
limiter = make(chan struct{}, srv.MaxConnections)
} else {
limiter = nil
}
for { for {
conn, e := l.Accept() conn, e := l.Accept()
@ -123,7 +132,19 @@ func (srv *Server) Serve(l net.Listener) error {
continue continue
} }
go session.serve() if limiter != nil {
go func() {
select {
case limiter <- struct{}{}:
session.serve()
<-limiter
default:
session.reject()
}
}()
} else {
go session.serve()
}
} }
@ -135,6 +156,10 @@ func (srv *Server) configureDefaults() {
srv.MaxMessageSize = 10240000 srv.MaxMessageSize = 10240000
} }
if srv.MaxConnections == 0 {
srv.MaxConnections = 100
}
if srv.ReadTimeout == 0 { if srv.ReadTimeout == 0 {
srv.ReadTimeout = time.Second * 60 srv.ReadTimeout = time.Second * 60
} }
@ -177,6 +202,11 @@ func (session *session) serve() {
} }
func (session *session) reject() {
session.reply(450, "Too busy. Try again later.")
session.close()
}
func (session *session) reply(code int, message string) { func (session *session) reply(code int, message string) {
fmt.Fprintf(session.writer, "%d %s\r\n", code, message) fmt.Fprintf(session.writer, "%d %s\r\n", code, message)

View file

@ -233,7 +233,7 @@ func TestSenderCheck(t *testing.T) {
defer ln.Close() defer ln.Close()
server := &Server{ server := &Server{
SenderChecker: func(peer Peer, addr MailAddress) error { return errors.New("Denied") }, SenderChecker: func(peer Peer, addr string) error { return errors.New("Denied") },
} }
go func() { go func() {
@ -261,7 +261,7 @@ func TestRecipientCheck(t *testing.T) {
defer ln.Close() defer ln.Close()
server := &Server{ server := &Server{
RecipientChecker: func(peer Peer, addr MailAddress) error { return errors.New("Denied") }, RecipientChecker: func(peer Peer, addr string) error { return errors.New("Denied") },
} }
go func() { go func() {
@ -398,3 +398,33 @@ func TestHandler(t *testing.T) {
} }
} }
func TestMaxConnections(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen failed: %v", err)
}
defer ln.Close()
server := &Server{
MaxConnections: 1,
}
go func() {
server.Serve(ln)
}()
c1, err := smtp.Dial(ln.Addr().String())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
_, err = smtp.Dial(ln.Addr().String())
if err == nil {
t.Fatal("Dial succeeded despite MaxConnections = 1")
}
c1.Close()
}