Add the first test for SMTP connections, based on the exchange in RFC 5321.
authorRobert Sesek <rsesek@bluestatic.org>
Tue, 13 Dec 2016 02:31:41 +0000 (21:31 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Tue, 13 Dec 2016 02:31:41 +0000 (21:31 -0500)
This fixes a few bugs caught by the test.

smtp/conn.go
smtp/conn_test.go [new file with mode: 0644]
smtp/server.go

index f29b198607f773f49e10a980ead6627727b722c4..ac075951cdee04298ba8ca6537f610bb63361d1e 100644 (file)
@@ -41,7 +41,7 @@ func AcceptConnection(netConn net.Conn, server Server) error {
 
        var err error
 
-       conn.writeReply(250, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)", server.Name(), netConn.LocalAddr()))
+       conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)", server.Name(), netConn.LocalAddr()))
 
        for {
                conn.line, err = conn.tp.ReadLine()
@@ -58,6 +58,7 @@ func AcceptConnection(netConn net.Conn, server Server) error {
 
                switch cmd {
                case "QUIT":
+                       conn.writeReply(221, "Goodbye")
                        conn.tp.Close()
                        break
                case "HELO":
diff --git a/smtp/conn_test.go b/smtp/conn_test.go
new file mode 100644 (file)
index 0000000..d0998cd
--- /dev/null
@@ -0,0 +1,108 @@
+package smtp
+
+import (
+       "net"
+       "net/textproto"
+       "path/filepath"
+       "runtime"
+       "strings"
+       "testing"
+)
+
+func ok(t testing.TB, err error) {
+       if err != nil {
+               _, file, line, _ := runtime.Caller(1)
+               t.Errorf("[%s:%d] unexpected error: %v", filepath.Base(file), line, err)
+       }
+}
+
+func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
+       _, message, err := conn.ReadCodeLine(code)
+       ok(t, err)
+       return message
+}
+
+// runServer creates a TCP socket, runs a listening server, and returns the connection.
+// The server exits when the Conn is closed.
+func runServer(t *testing.T, server Server) net.Listener {
+       l, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatal(err)
+               return nil
+       }
+
+       go func() {
+               for {
+                       conn, err := l.Accept()
+                       if err != nil {
+                               return
+                       }
+                       go AcceptConnection(conn, server)
+               }
+       }()
+
+       return l
+}
+
+type testServer struct {
+       EmptyServerCallbacks
+}
+
+func (s *testServer) Name() string {
+       return "Test-Server"
+}
+
+func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
+       conn, err := textproto.Dial(addr.Network(), addr.String())
+       if err != nil {
+               t.Fatal(err)
+               return nil
+       }
+       return conn
+}
+
+// RFC 5321 ยง D.1
+func TestScenarioTypical(t *testing.T) {
+       s := testServer{}
+       l := runServer(t, &s)
+       defer l.Close()
+
+       conn := createClient(t, l.Addr())
+
+       message := readCodeLine(t, conn, 220)
+       if !strings.HasPrefix(message, s.Name()) {
+               t.Errorf("Greeting does not have server name, got %q", message)
+       }
+
+       greet := "greeting.TestScenarioTypical"
+       ok(t, conn.PrintfLine("EHLO "+greet))
+
+       _, message, err := conn.ReadResponse(250)
+       ok(t, err)
+       if !strings.Contains(message, greet) {
+               t.Errorf("EHLO response does not contain greeting, got %q", message)
+       }
+
+       ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
+       readCodeLine(t, conn, 250)
+
+       ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
+       readCodeLine(t, conn, 250)
+
+       ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
+       readCodeLine(t, conn, 250) // TODO: make this 55o by rejecting Green
+
+       ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
+       readCodeLine(t, conn, 250)
+
+       ok(t, conn.PrintfLine("DATA"))
+       readCodeLine(t, conn, 354)
+
+       ok(t, conn.PrintfLine("Blah blah blah..."))
+       ok(t, conn.PrintfLine("...etc. etc. etc."))
+       ok(t, conn.PrintfLine("."))
+       readCodeLine(t, conn, 250)
+
+       ok(t, conn.PrintfLine("QUIT"))
+       readCodeLine(t, conn, 221)
+}
index 271e172008e1f7831142bec80b7161ab567a32a4..3ecb24aff6cb3accc8e0b86c5db9fa9231997505 100644 (file)
@@ -31,3 +31,17 @@ type Server interface {
        OnEHLO() *ReplyLine
        OnMessageDelivered(Envelope) *ReplyLine
 }
+
+type EmptyServerCallbacks struct {}
+
+func (*EmptyServerCallbacks) TLSConfig() *tls.Config {
+       return nil
+}
+
+func (*EmptyServerCallbacks) OnEHLO() *ReplyLine {
+       return nil
+}
+
+func (*EmptyServerCallbacks) OnMessageDelivered(Envelope) *ReplyLine {
+       return nil
+}