Implemented MaxConnections, corrected examples, removed MailAddress.
This commit is contained in:
parent
587b6ad4ac
commit
64d201aecb
5 changed files with 84 additions and 27 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
42
smtpd.go
42
smtpd.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if limiter != nil {
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case limiter <- struct{}{}:
|
||||||
|
session.serve()
|
||||||
|
<-limiter
|
||||||
|
default:
|
||||||
|
session.reject()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
go session.serve()
|
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue