From e30fdeff7a6b211a79d00f69d3447f9f41e81ab0 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Sun, 20 Jul 2014 21:51:39 +0200 Subject: [PATCH] XCLIENT support. --- protocol.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++++-- smtpd.go | 15 +++++++ smtpd_test.go | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 4 deletions(-) diff --git a/protocol.go b/protocol.go index bf783a3..6caaf36 100644 --- a/protocol.go +++ b/protocol.go @@ -8,7 +8,9 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/textproto" + "strconv" "strings" "time" ) @@ -84,6 +86,10 @@ func (session *session) handle(line string) { session.handleAUTH(cmd) return + case "XCLIENT": + session.handleXCLIENT(cmd) + return + } session.reply(502, "Unsupported command.") @@ -111,6 +117,7 @@ func (session *session) handleHELO(cmd command) { } session.peer.HeloName = cmd.fields[1] + session.peer.Protocol = SMTP session.reply(250, "Go ahead") return @@ -138,6 +145,7 @@ func (session *session) handleEHLO(cmd command) { } session.peer.HeloName = cmd.fields[1] + session.peer.Protocol = ESMTP extensions := session.extensions() @@ -179,8 +187,10 @@ func (session *session) handleMAIL(cmd command) { if session.server.SenderChecker != nil { err = session.server.SenderChecker(session.peer, addr) - session.error(err) - return + if err != nil { + session.error(err) + return + } } session.envelope = &Envelope{ @@ -214,8 +224,10 @@ func (session *session) handleRCPT(cmd command) { if session.server.RecipientChecker != nil { err = session.server.RecipientChecker(session.peer, addr) - session.error(err) - return + if err != nil { + session.error(err) + return + } } session.envelope.Recipients = append(session.envelope.Recipients, addr) @@ -445,3 +457,102 @@ func (session *session) handleAUTH(cmd command) { session.reply(235, "OK, you are now authenticated") } + +func (session *session) handleXCLIENT(cmd command) { + + if !session.server.EnableXCLIENT { + session.reply(550, "XCLIENT not enabled") + return + } + + var ( + newHeloName = "" + newAddr net.IP = nil + newTCPPort uint64 = 0 + newUsername = "" + newProto Protocol = "" + ) + + for _, item := range cmd.fields[1:] { + + parts := strings.Split(item, "=") + + if len(parts) != 2 { + session.reply(502, "Couldn't decode the command.") + return + } + + name := parts[0] + value := parts[1] + + switch name { + + case "NAME": + // Unused in smtpd package + continue + + case "HELO": + newHeloName = value + continue + + case "ADDR": + newAddr = net.ParseIP(value) + continue + + case "PORT": + var err error + newTCPPort, err = strconv.ParseUint(value, 10, 16) + if err != nil { + session.reply(502, "Couldn't decode the command.") + return + } + continue + + case "LOGIN": + newUsername = value + continue + + case "PROTO": + if value == "SMTP" { + newProto = SMTP + } else if value == "ESMTP" { + newProto = ESMTP + } + continue + + default: + session.reply(502, "Couldn't decode the command.") + return + } + + } + + tcpAddr, ok := session.peer.Addr.(*net.TCPAddr) + if !ok { + session.reply(502, "Unsupported network connection") + return + } + + if newHeloName != "" { + session.peer.HeloName = newHeloName + } + + if newAddr != nil { + tcpAddr.IP = newAddr + } + + if newTCPPort != 0 { + tcpAddr.Port = int(newTCPPort) + } + + if newUsername != "" { + session.peer.Username = newUsername + } + + if newProto != "" { + session.peer.Protocol = newProto + } + + session.welcome() + +} diff --git a/smtpd.go b/smtpd.go index 490a546..3319baa 100644 --- a/smtpd.go +++ b/smtpd.go @@ -42,15 +42,26 @@ type Server struct { // Can be left empty for no authentication support. Authenticator func(peer Peer, username, password string) error + EnableXCLIENT bool // Enable XCLIENT support (default: false) + TLSConfig *tls.Config // Enable STARTTLS support. ForceTLS bool // Force STARTTLS usage. } +// Protocol represents the protocol used in the SMTP session +type Protocol string + +const ( + SMTP Protocol = "SMTP" + ESMTP = "ESMTP" +) + // Peer represents the client connecting to the server type Peer struct { HeloName string // Server name used in HELO/EHLO command Username string // Username from authentication, if authenticated Password string // Password from authentication, if authenticated + Protocol Protocol // Protocol used, SMTP or ESMTP Addr net.Addr // Network address } @@ -294,6 +305,10 @@ func (session *session) extensions() []string { "PIPELINING", } + if session.server.EnableXCLIENT { + extensions = append(extensions, "XCLIENT") + } + if session.server.TLSConfig != nil && !session.tls { extensions = append(extensions, "STARTTLS") } diff --git a/smtpd_test.go b/smtpd_test.go index a8b47a6..3770744 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -1058,3 +1058,90 @@ func TestLongLine(t *testing.T) { } } + +func TestXCLIENT(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 := &smtpd.Server{ + EnableXCLIENT: true, + SenderChecker: func(peer smtpd.Peer, addr string) error { + if peer.HeloName != "new.example.net" { + t.Fatalf("Didn't override HELO name: %v", peer.HeloName) + } + if peer.Addr.String() != "42.42.42.42:4242" { + t.Fatalf("Didn't override IP/Port: %v", peer.Addr) + } + if peer.Username != "newusername" { + t.Fatalf("Didn't override username: %v", peer.Username) + } + if peer.Protocol != smtpd.SMTP { + t.Fatalf("Didn't override protocol: %v", peer.Protocol) + } + return nil + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if supported, _ := c.Extension("XCLIENT"); !supported { + t.Fatal("XCLIENT not supported") + } + + id, err := c.Text.Cmd("XCLIENT NAME=ignored ADDR=42.42.42.42 PORT=4242 PROTO=SMTP HELO=new.example.net LOGIN=newusername") + if err != nil { + t.Fatalf("Cmd failed: %v", err) + } + + c.Text.StartResponse(id) + _, _, err = c.Text.ReadResponse(220) + c.Text.EndResponse(id) + + if err != nil { + t.Fatalf("XCLIENT failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("Mail failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("Rcpt failed: %v", err) + } + + if err := c.Rcpt("recipient2@example.net"); err != nil { + t.Fatalf("Rcpt2 failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("Quit failed: %v", err) + } + +}