From fbebcf96d80dcf2fa99985b7b727fef5250b3055 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 14:48:24 +0200 Subject: [PATCH] Add simple SMTP test server for unit testing Implemented a simple SMTP test server to facilitate unit testing. This server listens on a specified address and port, accepts connections, and processes common SMTP commands like HELO, MAIL FROM, RCPT TO, DATA, and QUIT. Several constants related to the server configuration were also added to the test file. --- client_test.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index efccea4..31b68bb 100644 --- a/client_test.go +++ b/client_test.go @@ -5,6 +5,7 @@ package mail import ( + "bufio" "context" "crypto/tls" "errors" @@ -21,11 +22,18 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// DefaultHost is used as default hostname for the Client -const DefaultHost = "localhost" - -// TestRcpt -const TestRcpt = "go-mail@mytrashmailer.com" +const ( + // DefaultHost is used as default hostname for the Client + DefaultHost = "localhost" + // TestRcpt is a trash mail address to send test mails to + TestRcpt = "go-mail@mytrashmailer.com" + // TestServerProto is the protocol used for the simple SMTP test server + TestServerProto = "tcp" + // TestServerAddr is the address the simple SMTP test server listens on + TestServerAddr = "127.0.0.1" + // TestServerPort is the port the simple SMTP test server listens on + TestServerPort = 2526 +) // TestNewClient tests the NewClient() method with its default options func TestNewClient(t *testing.T) { @@ -1545,3 +1553,141 @@ func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } + +func simpleSMTPServer(ctx context.Context, featureSet string) error { + listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) + if err != nil { + return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) + } + + defer func() { + fmt.Printf("closing listener\n") + if err := listener.Close(); err != nil { + fmt.Printf("unable to close listener: %s\n", err) + } + }() + + for { + select { + case <-ctx.Done(): + return nil + default: + connection, err := listener.Accept() + var opErr *net.OpError + if err != nil { + if errors.As(err, &opErr) && opErr.Temporary() { + continue + } + return fmt.Errorf("unable to accept connection: %w", err) + } + handleTestServerConnection(connection, featureSet) + } + } +} + +func handleTestServerConnection(connection net.Conn, featureSet string) { + defer func() { + if err := connection.Close(); err != nil { + fmt.Printf("unable to close connection: %s\n", err) + } + }() + + reader := bufio.NewReader(connection) + writer := bufio.NewWriter(connection) + + writeLine := func(data string) error { + _, err := writer.WriteString(data + "\r\n") + if err != nil { + return fmt.Errorf("unable to write line: %w", err) + } + return writer.Flush() + } + writeOK := func() { + _ = writeLine("250 2.0.0 OK") + } + + if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { + fmt.Printf("unable to write to client: %s\n", err) + return + } + + data, err := reader.ReadString('\n') + if err != nil { + fmt.Printf("unable to read from connection: %s\n", err) + } + if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { + fmt.Printf("expected EHLO, got %q", data) + os.Exit(1) + } + if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { + fmt.Printf("unable to write to connection: %s\n", err) + return + } + + for { + data, err = reader.ReadString('\n') + if err != nil { + if err == io.EOF { + fmt.Println("Connection closed by client.") + break + } + fmt.Println("Error reading data:", err) + break + } + + var datastring string + data = strings.TrimSpace(data) + switch { + case strings.HasPrefix(data, "MAIL FROM:"): + from := strings.TrimPrefix(data, "MAIL FROM:") + from = strings.ReplaceAll(from, "BODY=8BITMIME", "") + from = strings.ReplaceAll(from, "SMTPUTF8", "") + from = strings.TrimSpace(from) + if !strings.EqualFold(from, "") { + _ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) + break + } + writeOK() + case strings.HasPrefix(data, "RCPT TO:"): + to := strings.TrimPrefix(data, "RCPT TO:") + if !strings.EqualFold(to, "") { + _ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) + break + } + writeOK() + case strings.HasPrefix(data, "AUTH PLAIN"): + auth := strings.TrimPrefix(data, "AUTH PLAIN ") + if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") { + _ = writeLine("535 5.7.8 Error: authentication failed") + break + } + _ = writeLine("235 2.7.0 Authentication successful") + case strings.EqualFold(data, "DATA"): + _ = writeLine("354 End data with .") + for { + ddata, derr := reader.ReadString('\n') + if derr != nil { + fmt.Printf("failed to read DATA data from connection: %s\n", derr) + break + } + ddata = strings.TrimSpace(ddata) + if ddata == "." { + _ = writeLine("250 2.0.0 Ok: queued as 1234567890") + break + } + datastring += ddata + "\n" + } + case strings.EqualFold(data, "noop"), + strings.EqualFold(data, "rset"), + strings.EqualFold(data, "vrfy"): + writeOK() + break + case strings.EqualFold(data, "quit"): + _ = writeLine("221 2.0.0 Bye") + break + default: + _ = writeLine("500 5.5.2 Error: bad syntax") + } + fmt.Printf("DATA received: %s", datastring) + } +}