mirror of
https://github.com/axllent/mailpit.git
synced 2025-05-15 22:16:44 +02:00
1600 lines
58 KiB
Go
1600 lines
58 KiB
Go
package smtpd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var cert = makeCertificate()
|
|
|
|
// Create a client to run commands with. Parse the banner for 220 response.
|
|
func newConn(t *testing.T, server *Server) net.Conn {
|
|
clientConn, serverConn := net.Pipe()
|
|
session := server.newSession(serverConn)
|
|
go session.serve()
|
|
|
|
banner, err := bufio.NewReader(clientConn).ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("Failed to read banner from test server: %v", err)
|
|
}
|
|
if banner[0:3] != "220" {
|
|
t.Fatalf("Read incorrect banner from test server: %v", banner)
|
|
}
|
|
return clientConn
|
|
}
|
|
|
|
// Send a command and verify the 3 digit code from the response.
|
|
func cmdCode(t *testing.T, conn net.Conn, cmd string, code string) string {
|
|
fmt.Fprintf(conn, "%s\r\n", cmd)
|
|
resp, err := bufio.NewReader(conn).ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("Failed to read response from test server: %v", err)
|
|
}
|
|
if resp[0:3] != code {
|
|
t.Errorf("Command \"%s\" response code is %s, want %s", cmd, resp[0:3], code)
|
|
}
|
|
return strings.TrimSpace(resp)
|
|
}
|
|
|
|
// Simple tests: connect, send command, then send QUIT.
|
|
// RFC 2821 section 4.1.4 specifies that these commands do not require a prior EHLO,
|
|
// only that clients should send one, so test without EHLO.
|
|
func TestSimpleCommands(t *testing.T) {
|
|
tests := []struct {
|
|
cmd string
|
|
code string
|
|
}{
|
|
{"NOOP", "250"},
|
|
{"RSET", "250"},
|
|
{"HELP", "502"},
|
|
{"VRFY", "502"},
|
|
{"EXPN", "502"},
|
|
{"TEST", "500"}, // Unsupported command
|
|
{"", "500"}, // Blank command
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
conn := newConn(t, &Server{})
|
|
cmdCode(t, conn, tt.cmd, tt.code)
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
}
|
|
|
|
func TestCmdHELO(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
|
|
// Send HELO, expect greeting.
|
|
cmdCode(t, conn, "HELO host.example.com", "250")
|
|
|
|
// Verify that HELO resets the current transaction state like RSET.
|
|
// RFC 2821 section 4.1.4 says EHLO should cause a reset, so verify that HELO does it too.
|
|
cmdCode(t, conn, "mail from:<sender@example.com>", "250") // Also testing case-insensitivity
|
|
cmdCode(t, conn, "rcpt to:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "HELO host.example.com", "250")
|
|
cmdCode(t, conn, "DATA", "503")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdEHLO(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
|
|
// Send EHLO, expect greeting.
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// Verify that EHLO resets the current transaction state like RSET.
|
|
// See RFC 2821 section 4.1.4 for more detail.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
cmdCode(t, conn, "DATA", "503")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdRSET(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// Verify that RSET clears the current transaction state.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "RSET", "250")
|
|
cmdCode(t, conn, "DATA", "503")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdMAIL(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// MAIL with no FROM arg should return 501 syntax error
|
|
cmdCode(t, conn, "MAIL", "501")
|
|
// MAIL with empty FROM arg should return 501 syntax error
|
|
cmdCode(t, conn, "MAIL FROM:", "501")
|
|
cmdCode(t, conn, "MAIL FROM: ", "501")
|
|
cmdCode(t, conn, "MAIL FROM: ", "501")
|
|
// MAIL with DSN-style FROM arg should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<>", "250")
|
|
// MAIL with valid FROM arg should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
|
|
// MAIL with seemingly valid but noncompliant FROM arg (single space after the colon) should be tolerated and should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM: <sender@example.com>", "250")
|
|
// MAIL with seemingly valid but noncompliant FROM arg (double space after the colon) should return 501 syntax error
|
|
cmdCode(t, conn, "MAIL FROM: <sender@example.com>", "501")
|
|
|
|
// MAIL with valid SIZE parameter should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE=1000", "250")
|
|
|
|
// MAIL with bad size parameter should return 501 syntax error
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE=", "501")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE= ", "501")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE=foo", "501")
|
|
|
|
// MAIL with options should be ignored except for SIZE
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> BODY=8BITMIME", "250") // ignored
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> BODY=8BITMIME,SIZE=1000", "250") // size detected
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> BODY=8BITMIME,SIZE=foo", "501") // ignored
|
|
|
|
// TODO: MAIL with valid AUTH parameter should return 250 Ok
|
|
|
|
// TODO: MAIL with invalid AUTH parameter must return 501 syntax error
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdMAILMaxSize(t *testing.T) {
|
|
maxSize := 10 + time.Now().Minute()
|
|
conn := newConn(t, &Server{MaxSize: maxSize})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// MAIL with no size parameter should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
|
|
// MAIL with bad size parameter should return 501 syntax error
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE=", "501")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE= ", "501")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE=foo", "501")
|
|
|
|
// MAIL with size parameter zero should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com> SIZE=0", "250")
|
|
|
|
// MAIL below the maximum size should return 250 Ok
|
|
cmdCode(t, conn, fmt.Sprintf("MAIL FROM:<sender@example.com> SIZE=%d", maxSize-1), "250")
|
|
|
|
// MAIL matching the maximum size should return 250 Ok
|
|
cmdCode(t, conn, fmt.Sprintf("MAIL FROM:<sender@example.com> SIZE=%d", maxSize), "250")
|
|
|
|
// MAIL above the maximum size should return a maximum size exceeded error.
|
|
cmdCode(t, conn, fmt.Sprintf("MAIL FROM:<sender@example.com> SIZE=%d", maxSize+1), "552")
|
|
|
|
// Clients should send either RSET or QUIT after receiving 552 (RFC 1870 section 6.2).
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdRCPT(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// RCPT without prior MAIL should return 503 bad sequence
|
|
cmdCode(t, conn, "RCPT", "503")
|
|
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
|
|
// RCPT with no TO arg should return 501 syntax error
|
|
cmdCode(t, conn, "RCPT", "501")
|
|
|
|
// RCPT with empty TO arg should return 501 syntax error
|
|
cmdCode(t, conn, "RCPT TO:", "501")
|
|
cmdCode(t, conn, "RCPT TO: ", "501")
|
|
cmdCode(t, conn, "RCPT TO: ", "501")
|
|
|
|
// RCPT with valid TO arg should return 250 Ok
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
|
|
// Up to 100 valid recipients should return 250 Ok
|
|
for i := 2; i < 101; i++ {
|
|
cmdCode(t, conn, fmt.Sprintf("RCPT TO:<recipient%v@example.com>", i), "250")
|
|
}
|
|
|
|
// 101st valid recipient with valid TO arg should return 452 too many recipients
|
|
cmdCode(t, conn, "RCPT TO:<recipient101@example.com>", "452")
|
|
|
|
// RCPT with valid TO arg and prior DSN-style FROM arg should return 250 Ok
|
|
cmdCode(t, conn, "RSET", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
|
|
// RCPT with seemingly valid but noncompliant TO arg (single space after the colon) should be tolerated and should return 250 Ok
|
|
cmdCode(t, conn, "RSET", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<>", "250")
|
|
cmdCode(t, conn, "RCPT TO: <recipient@example.com>", "250")
|
|
|
|
// RCPT with seemingly valid but noncompliant TO arg (double space after the colon) should return 501 syntax error
|
|
cmdCode(t, conn, "RSET", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<>", "250")
|
|
cmdCode(t, conn, "RCPT TO: <recipient@example.com>", "501")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdMaxRecipients(t *testing.T) {
|
|
conn := newConn(t, &Server{MaxRecipients: 3})
|
|
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
|
|
cmdCode(t, conn, "RCPT TO: <recipient1@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO: <recipient2@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO: <recipient3@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO: <recipient4@example.com>", "452")
|
|
cmdCode(t, conn, "RCPT TO: <recipient5@example.com>", "452")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdDATA(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// DATA without prior MAIL & RCPT should return 503 bad sequence
|
|
cmdCode(t, conn, "DATA", "503")
|
|
cmdCode(t, conn, "RSET", "250")
|
|
|
|
// DATA without prior RCPT should return 503 bad sequence
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "503")
|
|
cmdCode(t, conn, "RSET", "250")
|
|
|
|
// Test a full mail transaction.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message.\r\n.", "250")
|
|
|
|
// Test a full mail transaction with a bad last recipient.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:", "501")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message.\r\n.", "250")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdDATAWithMaxSize(t *testing.T) {
|
|
// "Test message.\r\n." is 15 bytes after trailing period is removed.
|
|
conn := newConn(t, &Server{MaxSize: 15})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// Messages below the maximum size should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message\r\n.", "250")
|
|
|
|
// Messages matching the maximum size should return 250 Ok
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message.\r\n.", "250")
|
|
|
|
// Messages above the maximum size should return a maximum size exceeded error.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message that is too long.\r\n.", "552")
|
|
|
|
// Clients should send either RSET or QUIT after receiving 552 (RFC 1870 section 6.2).
|
|
cmdCode(t, conn, "RSET", "250")
|
|
|
|
// Messages above the maximum size should return a maximum size exceeded error.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message.\r\nSecond line that is too long.\r\n.", "552")
|
|
|
|
// Clients should send either RSET or QUIT after receiving 552 (RFC 1870 section 6.2).
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
type mockHandler struct {
|
|
handlerCalled int
|
|
}
|
|
|
|
func (m *mockHandler) handler(err error) func(a net.Addr, f string, t []string, d []byte) error {
|
|
return func(a net.Addr, f string, t []string, d []byte) error {
|
|
m.handlerCalled++
|
|
return err
|
|
}
|
|
}
|
|
|
|
func TestCmdDATAWithHandler(t *testing.T) {
|
|
m := mockHandler{}
|
|
conn := newConn(t, &Server{Handler: m.handler(nil)})
|
|
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message.\r\n.", "250")
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
|
|
if m.handlerCalled != 1 {
|
|
t.Errorf("MailHandler called %d times, want one call", m.handlerCalled)
|
|
}
|
|
}
|
|
|
|
func TestCmdDATAWithHandlerError(t *testing.T) {
|
|
m := mockHandler{}
|
|
conn := newConn(t, &Server{Handler: m.handler(errors.New("Handler error"))})
|
|
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message.\r\n.", "451")
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
|
|
if m.handlerCalled != 1 {
|
|
t.Errorf("MailHandler called %d times, want one call", m.handlerCalled)
|
|
}
|
|
}
|
|
|
|
func TestCmdSTARTTLS(t *testing.T) {
|
|
conn := newConn(t, &Server{})
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// By default, TLS is not configured, so STARTTLS should return 502 not implemented.
|
|
cmdCode(t, conn, "STARTTLS", "502")
|
|
|
|
// Parameters are not allowed (RFC 3207 section 4).
|
|
cmdCode(t, conn, "STARTTLS FOO", "501")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdSTARTTLSFailure(t *testing.T) {
|
|
// Deliberately misconfigure TLS to force a handshake failure.
|
|
server := &Server{TLSConfig: &tls.Config{}}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// When TLS is configured, STARTTLS should return 220 Ready to start TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
|
|
// A failed TLS handshake should return 403 TLS handshake failed
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
reader := bufio.NewReader(conn)
|
|
resp, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatalf("Failed to read response after failed TLS handshake: %v", err)
|
|
}
|
|
if resp[0:3] != "403" {
|
|
t.Errorf("Failed TLS handshake response code is %s, want 403", resp[0:3])
|
|
}
|
|
} else {
|
|
t.Error("TLS handshake succeeded with empty tls.Config, want failure")
|
|
}
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
// Utility function to make a valid TLS certificate for use by the server.
|
|
func makeCertificate() tls.Certificate {
|
|
const certPEM = `
|
|
-----BEGIN CERTIFICATE-----
|
|
MIID9DCCAtygAwIBAgIJAIX/1sxuqZKrMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV
|
|
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
|
aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzA1MDYxNDIy
|
|
MjVaFw0yNzA1MDQxNDIyMjVaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21l
|
|
LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
|
|
BAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALO4
|
|
XVY5Kw9eNblqBenC03Wz6qemLFw8zLDNrehvjYuJPn5WVwvzLNP+3S02iqQD+Y1k
|
|
vszqDIZLQdjWLiEZdtxfemyIr+RePIMclnceGYFx3Zgg5qeyvOWlJLM41ZU8YZb/
|
|
zGj3RtXzuOZ5vePSLGS1nudjrKSBs7shRY8bYjkOqFujsSVnEK7s3Kb2Sf/rO+7N
|
|
RZ1df3hhyKtyq4Pb5eC1mtQqcRjRSZdTxva8kO4vRQbvGgjLUakvBVrrnwbww5a4
|
|
2wKbQPKIClEbSLyKQ62zR8gW1rPwBdokd8u9+rLbcmr7l0OuAsSn5Xi9x6VxXTNE
|
|
bgCa1KVoE4bpoGG+KQsCAwEAAaOBvjCBuzAdBgNVHQ4EFgQUILso/fozIhaoyi05
|
|
XNSWzP/ck+4wgYsGA1UdIwSBgzCBgIAUILso/fozIhaoyi05XNSWzP/ck+6hXaRb
|
|
MFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJ
|
|
bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAIX/
|
|
1sxuqZKrMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIbzsvTZb8LA
|
|
JqyaTttsMMA1szf4WBX88lVWbIk91k0nlTa0BiU/UocKrU6c9PySwJ6FOFJpgpdH
|
|
z/kmJ+S+d4pvgqBzWbKMoMrNlMt6vL+H8Mbf/l/CN91eNM+gJZu2HgBIFGW1y4Wy
|
|
gOzjEm9bw15Hgqqs0P4CSy7jcelWA285DJ7IG1qdPGhAKxT4/UuDin8L/u2oeYWH
|
|
3DwTDO4kAUnKetcmNQFSX3Ge50uQypl8viYgFJ2axOfZ3imjQZrs7M1Og6Wnj/SD
|
|
F414wVQibsZyZp8cqwR/OinvxloPkPVnf163jPRtftuqezEY8Nyj83O5u5sC1Azs
|
|
X/Gm54QNk6w=
|
|
-----END CERTIFICATE-----`
|
|
const keyPEM = `
|
|
-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEAs7hdVjkrD141uWoF6cLTdbPqp6YsXDzMsM2t6G+Ni4k+flZX
|
|
C/Ms0/7dLTaKpAP5jWS+zOoMhktB2NYuIRl23F96bIiv5F48gxyWdx4ZgXHdmCDm
|
|
p7K85aUkszjVlTxhlv/MaPdG1fO45nm949IsZLWe52OspIGzuyFFjxtiOQ6oW6Ox
|
|
JWcQruzcpvZJ/+s77s1FnV1/eGHIq3Krg9vl4LWa1CpxGNFJl1PG9ryQ7i9FBu8a
|
|
CMtRqS8FWuufBvDDlrjbAptA8ogKURtIvIpDrbNHyBbWs/AF2iR3y736sttyavuX
|
|
Q64CxKfleL3HpXFdM0RuAJrUpWgThumgYb4pCwIDAQABAoIBAHzvYntJPKTvUhu2
|
|
F6w8kvHVBABNpbLtVUJniUj3G4fv/bCn5tVY1EX/e9QtgU2psbbYXUdoQRKuiHTr
|
|
15+M6zMhcKK4lsYDuL9QhU0DcKmq9WgHHzFfMK/YEN5CWT/ofNMSuhASLn0Xc+dM
|
|
pHQWrGPKWk/y25Z0z/P7mjZ0y+BrJOKlxV53A2AWpj4JtjX2YO6s/eiraFX+RNlv
|
|
GyWzeQ7Gynm2TD9VXhS+m40VVBmmbbeZYDlziDoWWNe9r26A+C8K65gZtjKdarMd
|
|
0LN89jJvI1pUxcIuvZJnumWUenZ7JhfBGpkfAwLB+MogUo9ekAHv1IZv/m3uWq9f
|
|
Zml2dZECgYEA2OCI8kkLRa3+IodqQNFrb/uZ16YouQ71B7nBgAxls9nuhyELKO7d
|
|
fzf1snPx6cbaCQKTyxrlYvck4gz8P09R7nVYwJuTmP0+QIgeCCc3Y9A2dyExaC6I
|
|
uKkFzJEqIVZNLvdjBRWQs5AiD1w58oto+wOvbagAQM483WiJ/qFaHCMCgYEA1CPo
|
|
zwI6pCn39RSYffK25HXM1q3i8ypkYdNsG6IVqS2FqHqj8XJSnDvLeIm7W1Rtw+uM
|
|
QdZ5O6PH31XgolG6LrFkW9vtfH+QnXQA2AnZQEfn034YZubhcexLqAkS9r0FUUZp
|
|
a1WI2jSxBBeB+to6MdNABuQOL3NHjPUidUKnOfkCgYA+HvKbE7ka2F+23DrfHh08
|
|
EkFat8lqWJJvCBIY73QiNAZSxnA/5UukqQ7DctqUL9U8R3S19JpH4qq55SZLrBi3
|
|
yP0HDokUhVVTfqm7hCAlgvpW3TcdtFaNLjzu/5WlvuaU0V+XkTnFdT+MTsp6YtxL
|
|
Kh8RtdF8vpZIhS0htm3tKQKBgQDQXoUp79KRtPdsrtIpw+GI/Xw50Yp9tkHrJLOn
|
|
YMlN5vzFw9CMM/KYqtLsjryMtJ0sN40IjhV+UxzbbYq7ZPMvMeaVo6vdAZ+WSH8b
|
|
tHDEBtzai5yEVntSXvrhDiimWnuCnVqmptlJG0BT+JMfRoKqtgjJu++DBARfm9hA
|
|
vTtsYQKBgE1ttTzd3HJoIhBBSvSMbyDWTED6jecKvsVypb7QeDxZCbIwCkoK9zn1
|
|
twPDHLBcUNhHJx6JWTR6BxI5DZoIA1tcKHtdO5smjLWNSKhXTsKWee2aNkZJkNIW
|
|
TDHSaTMOxVUEzpx84xClf561BTiTgzQy2MULpg3AK0Cv9l0+Yrvz
|
|
-----END RSA PRIVATE KEY-----`
|
|
|
|
cert, _ := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
|
|
return cert
|
|
}
|
|
|
|
func TestCmdSTARTTLSSuccess(t *testing.T) {
|
|
// Configure a valid TLS certificate so the handshake will succeed.
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// When TLS is configured, STARTTLS should return 220 Ready to start TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
|
|
// A successful TLS handshake shouldn't return anything, it should wait for EHLO.
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
|
|
// The subsequent EHLO should be successful.
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// When TLS is already in use, STARTTLS should return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "STARTTLS", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestCmdSTARTTLSRequired(t *testing.T) {
|
|
tests := []struct {
|
|
cmd string
|
|
codeBefore string
|
|
codeAfter string
|
|
}{
|
|
{"EHLO host.example.com", "250", "250"},
|
|
{"NOOP", "250", "250"},
|
|
{"MAIL FROM:<sender@example.com>", "530", "250"},
|
|
{"RCPT TO:<recipient@example.com>", "530", "250"},
|
|
{"RSET", "530", "250"}, // Reset before DATA to avoid having to actually send a message.
|
|
{"DATA", "530", "503"},
|
|
{"HELP", "502", "502"},
|
|
{"VRFY", "502", "502"},
|
|
{"EXPN", "502", "502"},
|
|
{"TEST", "500", "500"}, // Unsupported command
|
|
{"", "500", "500"}, // Blank command
|
|
{"AUTH", "530", "502"}, // AuthHandler not configured
|
|
}
|
|
|
|
// If TLS is not configured, the TLSRequired setting is ignored, so it must be configured for this test.
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, TLSRequired: true}
|
|
conn := newConn(t, server)
|
|
|
|
// If TLS is required, but not in use, reject every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207 section 4.
|
|
for _, tt := range tests {
|
|
cmdCode(t, conn, tt.cmd, tt.codeBefore)
|
|
}
|
|
|
|
// Switch to using TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
|
|
// A successful TLS handshake shouldn't return anything, it should wait for EHLO.
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
|
|
// The subsequent EHLO should be successful.
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// If TLS is required, and is in use, every command should work normally.
|
|
for _, tt := range tests {
|
|
cmdCode(t, tlsConn, tt.cmd, tt.codeAfter)
|
|
}
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestMakeHeaders(t *testing.T) {
|
|
now := time.Now().Format("Mon, 2 Jan 2006 15:04:05 -0700 (MST)")
|
|
valid := "Received: from clientName (clientHost [clientIP])\r\n" +
|
|
" by serverName (smtpd) with SMTP\r\n" +
|
|
" for <recipient@example.com>; " +
|
|
fmt.Sprintf("%s\r\n", now)
|
|
|
|
srv := &Server{AppName: "smtpd", Hostname: "serverName"}
|
|
s := &session{srv: srv, remoteIP: "clientIP", remoteHost: "clientHost", remoteName: "clientName"}
|
|
headers := s.makeHeaders([]string{"recipient@example.com"})
|
|
if string(headers) != valid {
|
|
t.Errorf("makeHeaders() returned\n%v, want\n%v", string(headers), valid)
|
|
}
|
|
}
|
|
|
|
// Test parsing of commands into verbs and arguments.
|
|
func TestParseLine(t *testing.T) {
|
|
tests := []struct {
|
|
line string
|
|
verb string
|
|
args string
|
|
}{
|
|
{"EHLO host.example.com", "EHLO", "host.example.com"},
|
|
{"MAIL FROM:<sender@example.com>", "MAIL", "FROM:<sender@example.com>"},
|
|
{"RCPT TO:<recipient@example.com>", "RCPT", "TO:<recipient@example.com>"},
|
|
{"QUIT", "QUIT", ""},
|
|
}
|
|
s := &session{}
|
|
for _, tt := range tests {
|
|
verb, args := s.parseLine(tt.line)
|
|
if verb != tt.verb || args != tt.args {
|
|
t.Errorf("ParseLine(%v) returned %v, %v, want %v, %v", tt.line, verb, args, tt.verb, tt.args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test reading of complete lines from the socket.
|
|
func TestReadLine(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
s := &session{}
|
|
s.srv = &Server{}
|
|
s.br = bufio.NewReader(&buf)
|
|
|
|
// Ensure readLine() returns an EOF error on an empty buffer.
|
|
_, err := s.readLine()
|
|
if err != io.EOF {
|
|
t.Errorf("readLine() on empty buffer returned err: %v, want EOF", err)
|
|
}
|
|
|
|
// Ensure trailing <CRLF> is stripped.
|
|
line := "FOO BAR BAZ\r\n"
|
|
cmd := "FOO BAR BAZ"
|
|
buf.Write([]byte(line))
|
|
output, err := s.readLine()
|
|
if err != nil {
|
|
t.Errorf("readLine(%v) returned err: %v", line, err)
|
|
} else if output != cmd {
|
|
t.Errorf("readLine(%v) returned %v, want %v", line, output, cmd)
|
|
}
|
|
}
|
|
|
|
// Test reading of message data, including dot stuffing (see RFC 5321 section 4.5.2).
|
|
func TestReadData(t *testing.T) {
|
|
tests := []struct {
|
|
lines string
|
|
data string
|
|
}{
|
|
// Single line message.
|
|
{"Test message.\r\n.\r\n", "Test message.\r\n"},
|
|
|
|
// Single line message with leading period removed.
|
|
{".Test message.\r\n.\r\n", "Test message.\r\n"},
|
|
|
|
// Multiple line message.
|
|
{"Line 1.\r\nLine 2.\r\nLine 3.\r\n.\r\n", "Line 1.\r\nLine 2.\r\nLine 3.\r\n"},
|
|
|
|
// Multiple line message with leading period removed.
|
|
{"Line 1.\r\n.Line 2.\r\nLine 3.\r\n.\r\n", "Line 1.\r\nLine 2.\r\nLine 3.\r\n"},
|
|
|
|
// Multiple line message with one leading period removed.
|
|
{"Line 1.\r\n..Line 2.\r\nLine 3.\r\n.\r\n", "Line 1.\r\n.Line 2.\r\nLine 3.\r\n"},
|
|
}
|
|
var buf bytes.Buffer
|
|
s := &session{}
|
|
s.srv = &Server{}
|
|
s.br = bufio.NewReader(&buf)
|
|
|
|
// Ensure readData() returns an EOF error on an empty buffer.
|
|
_, err := s.readData()
|
|
if err != io.EOF {
|
|
t.Errorf("readData() on empty buffer returned err: %v, want EOF", err)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
buf.Write([]byte(tt.lines))
|
|
data, err := s.readData()
|
|
if err != nil {
|
|
t.Errorf("readData(%v) returned err: %v", tt.lines, err)
|
|
} else if string(data) != tt.data {
|
|
t.Errorf("readData(%v) returned %v, want %v", tt.lines, string(data), tt.data)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test reading of message data with maximum size set (see RFC 1870 section 6.3).
|
|
func TestReadDataWithMaxSize(t *testing.T) {
|
|
tests := []struct {
|
|
lines string
|
|
maxSize int
|
|
err error
|
|
}{
|
|
// Maximum size of zero (the default) should not return an error.
|
|
{"Test message.\r\n.\r\n", 0, nil},
|
|
|
|
// Messages below the maximum size should not return an error.
|
|
{"Test message.\r\n.\r\n", 16, nil},
|
|
|
|
// Messages matching the maximum size should not return an error.
|
|
{"Test message.\r\n.\r\n", 15, nil},
|
|
|
|
// Messages above the maximum size should return a maximum size exceeded error.
|
|
{"Test message.\r\n.\r\n", 14, maxSizeExceeded(14)},
|
|
}
|
|
var buf bytes.Buffer
|
|
s := &session{}
|
|
s.br = bufio.NewReader(&buf)
|
|
|
|
for _, tt := range tests {
|
|
s.srv = &Server{MaxSize: tt.maxSize}
|
|
buf.Write([]byte(tt.lines))
|
|
_, err := s.readData()
|
|
if err != tt.err {
|
|
t.Errorf("readData(%v) returned err: %v", tt.lines, tt.err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility function for parsing extensions listed as service extensions in response to an EHLO command.
|
|
func parseExtensions(t *testing.T, greeting string) map[string]string {
|
|
extensions := make(map[string]string)
|
|
lines := strings.Split(greeting, "\n")
|
|
|
|
if len(lines) > 1 {
|
|
iLast := len(lines) - 1
|
|
for i, line := range lines {
|
|
prefix := line[0:4]
|
|
|
|
// All but the last extension code prefix should be "250-".
|
|
if i != iLast && prefix != "250-" {
|
|
t.Errorf("Extension code prefix is %s, want '250-'", prefix)
|
|
}
|
|
|
|
// The last extension code prefix should be "250 ".
|
|
if i == iLast && prefix != "250 " {
|
|
t.Errorf("Extension code prefix is %s, want '250 '", prefix)
|
|
}
|
|
|
|
// Skip greeting line.
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
|
|
// Add line as extension.
|
|
line = strings.TrimSpace(line[4:]) // Strip code prefix and trailing \r\n
|
|
if idx := strings.Index(line, " "); idx != -1 {
|
|
extensions[line[:idx]] = line[idx+1:]
|
|
} else {
|
|
extensions[line] = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
return extensions
|
|
}
|
|
|
|
// Test handler function for validating authentication credentials.
|
|
// The secret parameter is passed as nil for LOGIN and PLAIN authentication mechanisms.
|
|
func testAuthHandler(_ net.Addr, _ string, username []byte, _ []byte, _ []byte) (bool, error) {
|
|
return string(username) == "valid", nil
|
|
}
|
|
|
|
// Test the extensions listed in response to an EHLO command.
|
|
func TestMakeEHLOResponse(t *testing.T) {
|
|
s := &session{}
|
|
s.srv = &Server{}
|
|
|
|
// Greeting should be returned without trailing newlines.
|
|
greeting := s.makeEHLOResponse()
|
|
if len(greeting) != len(strings.TrimSpace(greeting)) {
|
|
t.Errorf("EHLO greeting string has leading or trailing whitespace")
|
|
}
|
|
|
|
// By default, TLS is not configured, so STARTTLS should not appear.
|
|
extensions := parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["STARTTLS"]; ok {
|
|
t.Errorf("STARTTLS appears in the extension list when TLS is not configured")
|
|
}
|
|
|
|
// If TLS is configured, but not already in use, STARTTLS should appear.
|
|
s.srv.TLSConfig = &tls.Config{}
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["STARTTLS"]; !ok {
|
|
t.Errorf("STARTTLS does not appear in the extension list when TLS is configured")
|
|
}
|
|
|
|
// If TLS is already used on the connection, STARTTLS should not appear.
|
|
s.tls = true
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["STARTTLS"]; ok {
|
|
t.Errorf("STARTTLS appears in the extension list when TLS is already in use")
|
|
}
|
|
|
|
// Verify default SIZE extension is zero.
|
|
s.srv = &Server{}
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["SIZE"]; !ok {
|
|
t.Errorf("SIZE does not appear in the extension list")
|
|
} else if extensions["SIZE"] != "0" {
|
|
t.Errorf("SIZE appears in the extension list with incorrect parameter %s, want %s", extensions["SIZE"], "0")
|
|
}
|
|
|
|
// Verify configured maximum message size is listed correctly.
|
|
// Any integer will suffice, as long as it's not hardcoded.
|
|
maxSize := 10 + time.Now().Minute()
|
|
maxSizeStr := fmt.Sprintf("%d", maxSize)
|
|
s.srv = &Server{MaxSize: maxSize}
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["SIZE"]; !ok {
|
|
t.Errorf("SIZE does not appear in the extension list")
|
|
} else if extensions["SIZE"] != maxSizeStr {
|
|
t.Errorf("SIZE appears in the extension list with incorrect parameter %s, want %s", extensions["SIZE"], maxSizeStr)
|
|
}
|
|
|
|
// With no authentication handler configured, AUTH should not be advertised.
|
|
s.srv = &Server{}
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["AUTH"]; ok {
|
|
t.Errorf("AUTH appears in the extension list when no AuthHandler is specified")
|
|
}
|
|
|
|
// With an authentication handler configured, AUTH should be advertised.
|
|
s.srv = &Server{AuthHandler: testAuthHandler}
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if _, ok := extensions["AUTH"]; !ok {
|
|
t.Errorf("AUTH does not appear in the extension list when an AuthHandler is specified")
|
|
}
|
|
|
|
reLogin := regexp.MustCompile("\\bLOGIN\\b")
|
|
rePlain := regexp.MustCompile("\\bPLAIN\\b")
|
|
|
|
// RFC 4954 specifies that, without TLS in use, plaintext authentication mechanisms must not be advertised.
|
|
s.tls = false
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if reLogin.MatchString(extensions["AUTH"]) {
|
|
t.Errorf("AUTH mechanism LOGIN appears in the extension list when an AuthHandler is specified and TLS is not in use")
|
|
}
|
|
if rePlain.MatchString(extensions["AUTH"]) {
|
|
t.Errorf("AUTH mechanism PLAIN appears in the extension list when an AuthHandler is specified and TLS is not in use")
|
|
}
|
|
|
|
// RFC 4954 specifies that, with TLS in use, plaintext authentication mechanisms can be advertised.
|
|
s.tls = true
|
|
extensions = parseExtensions(t, s.makeEHLOResponse())
|
|
if !reLogin.MatchString(extensions["AUTH"]) {
|
|
t.Errorf("AUTH mechanism LOGIN does not appear in the extension list when an AuthHandler is specified and TLS is in use")
|
|
}
|
|
if !rePlain.MatchString(extensions["AUTH"]) {
|
|
t.Errorf("AUTH mechanism PLAIN does not appear in the extension list when an AuthHandler is specified and TLS is in use")
|
|
}
|
|
}
|
|
|
|
func createTmpFile(content string) (file *os.File, err error) {
|
|
file, err = os.CreateTemp("", "")
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = file.Write([]byte(content))
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = file.Close()
|
|
return
|
|
}
|
|
|
|
func createTLSFiles() (
|
|
certFile *os.File,
|
|
keyFile *os.File,
|
|
passphrase string,
|
|
err error,
|
|
) {
|
|
const certPEM = `-----BEGIN CERTIFICATE-----
|
|
MIIDRzCCAi+gAwIBAgIJAKtg4oViVwv4MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
|
|
BAMMCWxvY2FsaG9zdDAgFw0xODA0MjAxMzMxNTBaGA8yMDg2MDUwODEzMzE1MFow
|
|
FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
|
CgKCAQEA8h7vl0gUquis5jRtcnETyD+8WITZO0s53aIzp0Y+9HXiHW6FGJjbOZjM
|
|
IvozNVni+83QWKumRTgeSzIIW2j4V8iFMSNrvWmhmCKloesXS1aY6H979e01Ve8J
|
|
WAJFRe6vZJd6gC6Z/P+ELU3ie4Vtr1GYfkV7nZ6VFp5/V/5nxGFag5TUlpP5hcoS
|
|
9r2kvXofosVwe3x3udT8SEbv5eBD4bKeVyJs/RLbxSuiU1358Y1cDdVuHjcvfm3c
|
|
ajhheQ4vX9WXsk7LGGhnf1SrrPN/y+IDTXfvoHn+nJh4vMAB4yzQdE1V1N1AB8RA
|
|
0yBVJ6dwxRrSg4BFrNWhj3gfsvrA7wIDAQABo4GZMIGWMB0GA1UdDgQWBBQ4/ncp
|
|
befFuKH1hoYkPqLwuRrPRjAfBgNVHSMEGDAWgBQ4/ncpbefFuKH1hoYkPqLwuRrP
|
|
RjAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIGQDALBgNVHQ8EBAMCBaAwEwYD
|
|
VR0lBAwwCgYIKwYBBQUHAwEwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3
|
|
DQEBCwUAA4IBAQBJBetEXiEIzKAEpXGX87j6aUON51Fdf6BiLMCghuGKyhnaOG32
|
|
4KJhtvVoS3ZUKPylh9c2VdItYlhWp76zd7YKk+3xUOixWeTMQHIvCvRGTyFibOPT
|
|
mApwp2pEnJCe4vjUrBaRhiyI+xnB70cWVF2qeernlLUeJA1mfYyQLz+v06ebDWOL
|
|
c/hPVQFB94lEdiyjGO7RZfIe8KwcK48g7iv0LQU4+c9MoWM2ZsVM1AL2tHzokSeA
|
|
u64gDTW4K0Tzx1ab7KmOFXYUjbz/xWuReMt33EwDXAErKCjbVt2T55Qx8UoKzSh1
|
|
tY0KDHdnYOzgsm2HIj2xcJqbeylYQvckNnoC
|
|
-----END CERTIFICATE-----`
|
|
|
|
const keyPEM = `-----BEGIN RSA PRIVATE KEY-----
|
|
Proc-Type: 4,ENCRYPTED
|
|
DEK-Info: AES-256-CBC,C16BF8745B2CDB53AC2B1D7609893AA0
|
|
|
|
O13z7Yq7butaJmMfg9wRis9YnIDPsp4coYI6Ud+JGcP7iXoy95QMhovKWx25o1ol
|
|
tvUTsrsG27fHGf9qG02KizApIVtO9c1e0swCWzFrKRQX0JDiZDmilb9xosBNNst1
|
|
BOzOTRZEwFGSOCKZRBfSXyqC93TvLJ3DO9IUnKIeGt7upipvg29b/Dur/fyCy2WV
|
|
bLHXwUTDBm7j49yfoEyGkDjoB2QO9wgcgbacbnQJQ25fTFUwZpZJEJv6o1tRhoYM
|
|
ZMOhC9x1URmdHKN1+z2y5BrB6oNpParfeAMEvs/9FE6jJwYUR28Ql6Mhphfvr9W2
|
|
5Gxd3J65Ao9Vi2I5j5X6aBuNjyhXN3ScLjPG4lVZm9RU/uTPEt81pig/d5nSAjvF
|
|
Nfc08NuG3cnMyJSE/xScJ4D+GtX8U969wO4oKPCR4E/NFyXPR730ppupDFG6hzPD
|
|
PDmiszDtU438JAZ8AuFa1LkbyFnEW6KVD4h7VRr8YDjirCqnkgjNSI6dFY0NQ8H7
|
|
SyexB0lrceX6HZc+oNdAtkX3tYdzY3ExzUM5lSF1dkldnRbApLbqc4uuNIVXhXFM
|
|
dJnoPdKAzM6i+2EeVUxWNdafKDxnjVSHIHzHfIFJLQ4GS5rnz9keRFdyDjQL07tT
|
|
Lu9pPOmsadDXp7oSa81RgoCUfNZeR4jKpCk2BOft0L6ZSqwYFLcQHLIfJaGfn902
|
|
TUOTxHt0KzEUYeYSrXC2a6cyvXAd1YI7lOgy60qG89VHyCc2v5Bs4c4FNUDC/+Dj
|
|
4ZwogaAbSNkLaE0q3sYQRPdxSqLftyX0KitAgE7oGtdzBfe1cdBoozw3U67NEMMT
|
|
6qvk5j7RepPRSrapHtK5pMMdg5XpKFWcOXZ26VHVrDCj4JKdjVb4iyiQi94VveV0
|
|
w9+KcOtyrM7/jbQlCWnXpsIkP8VA/RIgh7CBn/h4oF1sO8ywP25OGQ7VWAVq1R9D
|
|
8bl8GzIdR9PZpFyOxuIac4rPa8tkDeoXKs4cxoao7H/OZO9o9aTB7CJMTL9yv0Kb
|
|
ntWuYxQchE6syoGsOgdGyZhaw4JeFkasDUP5beyNY+278NkzgGTOIMMTXIX46woP
|
|
ehzHKGHXVGf7ZiSFF+zAHMXZRSwNVMkOYwlIoRg1IbvIRbAXqAR6xXQTCVzNG0SU
|
|
cskojycBca1Cz3hDVIKYZd9beDhprVdr2a4K2nft2g2xRNjKPopsaqXx+VPibFUx
|
|
X7542eQ3eAlhkWUuXvt0q5a9WJdjJp9ODA0/d0akF6JQlEHIAyLfoUKB1HYwgUGG
|
|
6uRm651FDAab9U4cVC5PY1hfv/QwzpkNDkzgJAZ5SMOfZhq7IdBcqGd3lzPmq2FP
|
|
Vy1LVZIl3eM+9uJx5TLsBHH6NhMwtNhFCNa/5ksodQYlTvR8IrrgWlYg4EL69vjS
|
|
yt6HhhEN3lFCWvrQXQMp93UklbTlpVt6qcDXiC7HYbs3+EINargRd5Z+xL5i5vkN
|
|
f9k7s0xqhloWNPZcyOXMrox8L81WOY+sP4mVlGcfDRLdEJ8X2ofJpOAcwYCnjsKd
|
|
uEGsi+l2fTj/F+eZLE6sYoMprgJrbfeqtRWFguUgTn7s5hfU0tZ46al5d0vz8fWK
|
|
-----END RSA PRIVATE KEY-----`
|
|
|
|
passphrase = "test"
|
|
|
|
certFile, err = createTmpFile(certPEM)
|
|
if err != nil {
|
|
return
|
|
}
|
|
keyFile, err = createTmpFile(keyPEM)
|
|
return
|
|
}
|
|
|
|
func TestAuthMechs(t *testing.T) {
|
|
s := session{}
|
|
s.srv = &Server{}
|
|
|
|
// Validate that non-TLS (default) configuration does not allow plaintext authentication mechanisms.
|
|
correct := map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": true}
|
|
mechs := s.authMechs()
|
|
if !reflect.DeepEqual(mechs, correct) {
|
|
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
|
|
}
|
|
|
|
// Validate that TLS configuration allows plaintext authentication mechanisms.
|
|
correct = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": true}
|
|
s.tls = true
|
|
mechs = s.authMechs()
|
|
if !reflect.DeepEqual(mechs, correct) {
|
|
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
|
|
}
|
|
|
|
// Validate that overridden values take precedence over RFC compliance when not using TLS.
|
|
correct = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": false}
|
|
s.tls = false
|
|
s.srv.AuthMechs = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": false}
|
|
mechs = s.authMechs()
|
|
if !reflect.DeepEqual(mechs, correct) {
|
|
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
|
|
}
|
|
|
|
// Validate that overridden values take precedence over RFC compliance when using TLS.
|
|
correct = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": true}
|
|
s.tls = true
|
|
s.srv.AuthMechs = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": true}
|
|
mechs = s.authMechs()
|
|
if !reflect.DeepEqual(mechs, correct) {
|
|
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
|
|
}
|
|
|
|
// Validate ability to explicitly disallow all mechanisms.
|
|
correct = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": false}
|
|
s.srv.AuthMechs = map[string]bool{"LOGIN": false, "PLAIN": false, "CRAM-MD5": false}
|
|
mechs = s.authMechs()
|
|
if !reflect.DeepEqual(mechs, correct) {
|
|
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
|
|
}
|
|
|
|
// Validate ability to explicitly allow all mechanisms.
|
|
correct = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": true}
|
|
s.srv.AuthMechs = map[string]bool{"LOGIN": true, "PLAIN": true, "CRAM-MD5": true}
|
|
mechs = s.authMechs()
|
|
if !reflect.DeepEqual(mechs, correct) {
|
|
t.Errorf("authMechs() returned %v, want %v", mechs, correct)
|
|
}
|
|
}
|
|
|
|
func TestCmdAUTH(t *testing.T) {
|
|
server := &Server{}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// By default no authentication handler is configured, so AUTH should return 502 not implemented.
|
|
cmdCode(t, conn, "AUTH", "502")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHOptional(t *testing.T) {
|
|
server := &Server{AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH without mechanism parameter must return 501 syntax error.
|
|
cmdCode(t, conn, "AUTH", "501")
|
|
|
|
// AUTH with a supported mechanism should return 334.
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
|
|
// AUTH must support cancellation with '*' and return 501 syntax error.
|
|
cmdCode(t, conn, "*", "501")
|
|
|
|
// AUTH with an unsupported mechanism should return 504 unrecognized type.
|
|
cmdCode(t, conn, "AUTH FOO", "504")
|
|
|
|
// The LOGIN and PLAIN mechanisms require a TLS connection, and are disabled by default.
|
|
cmdCode(t, conn, "AUTH LOGIN", "504")
|
|
cmdCode(t, conn, "AUTH PLAIN", "504")
|
|
|
|
// AUTH attempt during a mail transaction must return 503 bad sequence.
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "503")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "503")
|
|
|
|
// AUTH after a mail transaction must return 334.
|
|
// TODO: Work out what should happen if AUTH is received after DATA.
|
|
cmdCode(t, conn, "DATA", "354")
|
|
cmdCode(t, conn, "Test message\r\n.", "250")
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
|
|
// Cancel the authentication attempt, otherwise the QUIT below will return 502.
|
|
// TODO: Work out what should happen if QUIT is received after AUTH.
|
|
cmdCode(t, conn, "*", "501")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHRequired(t *testing.T) {
|
|
server := &Server{AuthHandler: testAuthHandler, AuthRequired: true}
|
|
conn := newConn(t, server)
|
|
|
|
tests := []struct {
|
|
cmd string
|
|
codeBefore string
|
|
codeAfter string
|
|
}{
|
|
{"EHLO host.example.com", "250", "250"},
|
|
{"NOOP", "250", "250"},
|
|
{"MAIL FROM:<sender@example.com>", "530", "250"},
|
|
{"RCPT TO:<recipient@example.com>", "530", "250"},
|
|
{"RSET", "250", "250"}, // Reset before DATA to avoid having to actually send a message.
|
|
{"DATA", "530", "503"},
|
|
{"HELP", "502", "502"},
|
|
{"VRFY", "502", "502"},
|
|
{"EXPN", "502", "502"},
|
|
{"TEST", "500", "500"}, // Unsupported command
|
|
{"", "500", "500"}, // Blank command
|
|
{"STARTTLS", "502", "502"}, // TLS not configured
|
|
}
|
|
|
|
// If authentication is configured and required, but not already in use, reject every command except
|
|
// AUTH, EHLO, HELO, NOOP, RSET, or QUIT as per RFC 4954.
|
|
for _, tt := range tests {
|
|
cmdCode(t, conn, tt.cmd, tt.codeBefore)
|
|
}
|
|
|
|
// AUTH without mechanism parameter must return 501 syntax error.
|
|
cmdCode(t, conn, "AUTH", "501")
|
|
|
|
// AUTH with a supported mechanism should return 334.
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
|
|
// AUTH must support cancellation with '*' and return 501 syntax error.
|
|
cmdCode(t, conn, "*", "501")
|
|
|
|
// AUTH with an unsupported mechanism should return 504 unrecognized type.
|
|
cmdCode(t, conn, "AUTH FOO", "504")
|
|
|
|
// The LOGIN and PLAIN mechanisms require a TLS connection, and are disabled by default.
|
|
cmdCode(t, conn, "AUTH LOGIN", "504")
|
|
cmdCode(t, conn, "AUTH PLAIN", "504")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHLOGIN(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH LOGIN without TLS in use must return 504 unrecognised type.
|
|
cmdCode(t, conn, "AUTH LOGIN", "504")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH LOGIN with TLS in use can proceed.
|
|
|
|
// LOGIN authentication process:
|
|
// Client sends "AUTH LOGIN"
|
|
// Server sends "334 VXNlcm5hbWU6" (Base64-encoded "Username:").
|
|
// Client sends Base64-encoded username.
|
|
// Server sends "334 UGFzc3dvcmQ6" (Base64-encoded "Password:").
|
|
// Client sends Base64-encoded password.
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
validUsername := base64.StdEncoding.EncodeToString([]byte("valid"))
|
|
invalidUsername := base64.StdEncoding.EncodeToString([]byte("invalid"))
|
|
password := base64.StdEncoding.EncodeToString([]byte("password"))
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "334")
|
|
cmdCode(t, tlsConn, invalidBase64, "501")
|
|
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "334")
|
|
cmdCode(t, tlsConn, validUsername, "334")
|
|
cmdCode(t, tlsConn, invalidBase64, "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "334")
|
|
cmdCode(t, tlsConn, invalidUsername, "334")
|
|
cmdCode(t, tlsConn, password, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "334")
|
|
cmdCode(t, tlsConn, validUsername, "334")
|
|
cmdCode(t, tlsConn, password, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHLOGINFast(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH LOGIN without TLS in use must return 504 unrecognised type.
|
|
cmdCode(t, conn, "AUTH LOGIN", "504")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH LOGIN with TLS in use can proceed.
|
|
|
|
// Fast LOGIN authentication process:
|
|
// Client sends "AUTH LOGIN " plus Base64-encoded username.
|
|
// Server sends "334 UGFzc3dvcmQ6" (Base64-encoded "Password:").
|
|
// Client sends Base64-encoded password.
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
validUsername := base64.StdEncoding.EncodeToString([]byte("valid"))
|
|
invalidUsername := base64.StdEncoding.EncodeToString([]byte("invalid"))
|
|
password := base64.StdEncoding.EncodeToString([]byte("password"))
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN "+invalidBase64, "501")
|
|
|
|
cmdCode(t, tlsConn, "AUTH LOGIN "+validUsername, "334")
|
|
cmdCode(t, tlsConn, invalidBase64, "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN "+invalidUsername, "334")
|
|
cmdCode(t, tlsConn, password, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "334")
|
|
cmdCode(t, tlsConn, validUsername, "334")
|
|
cmdCode(t, tlsConn, password, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHPLAIN(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN without TLS in use must return 504 unrecognised type.
|
|
cmdCode(t, conn, "AUTH PLAIN", "504")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN with TLS in use can proceed.
|
|
// RFC 2595 specifies:
|
|
// The client sends the authorization identity (identity to
|
|
// login as), followed by a US-ASCII NUL character, followed by the
|
|
// authentication identity (identity whose password will be used),
|
|
// followed by a US-ASCII NUL character, followed by the clear-text
|
|
// password. The client may leave the authorization identity empty to
|
|
// indicate that it is the same as the authentication identity.
|
|
|
|
// PLAIN authentication process:
|
|
// Client sends "AUTH PLAIN"
|
|
// Server sends "334 " (RFC 4954 requires the space).
|
|
// Client sends Base64-encoded string: identity\0username\0password
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
missingNUL := base64.StdEncoding.EncodeToString([]byte("valid\x00password"))
|
|
valid := base64.StdEncoding.EncodeToString([]byte("identity\x00valid\x00password"))
|
|
invalid := base64.StdEncoding.EncodeToString([]byte("identity\x00invalid\x00password"))
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, invalidBase64, "501")
|
|
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, missingNUL, "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, invalid, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, valid, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHPLAINEmpty(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN without TLS in use must return 504 unrecognised type.
|
|
cmdCode(t, conn, "AUTH PLAIN", "504")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN with TLS in use can proceed.
|
|
// RFC 2595 specifies:
|
|
// The client sends the authorization identity (identity to
|
|
// login as), followed by a US-ASCII NUL character, followed by the
|
|
// authentication identity (identity whose password will be used),
|
|
// followed by a US-ASCII NUL character, followed by the clear-text
|
|
// password. The client may leave the authorization identity empty to
|
|
// indicate that it is the same as the authentication identity.
|
|
|
|
// PLAIN authentication process with empty authorisation identity:
|
|
// Client sends "AUTH PLAIN"
|
|
// Server sends "334 " (RFC 4954 requires the space).
|
|
// Client sends Base64-encoded string: \0username\0password
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
missingNUL := base64.StdEncoding.EncodeToString([]byte("valid\x00password"))
|
|
valid := base64.StdEncoding.EncodeToString([]byte("\x00valid\x00password"))
|
|
invalid := base64.StdEncoding.EncodeToString([]byte("\x00invalid\x00password"))
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, invalidBase64, "501")
|
|
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, missingNUL, "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, invalid, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "334")
|
|
cmdCode(t, tlsConn, valid, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHPLAINFast(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN without TLS in use must return 504 unrecognised type.
|
|
cmdCode(t, conn, "AUTH PLAIN", "504")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN with TLS in use can proceed.
|
|
// RFC 2595 specifies:
|
|
// The client sends the authorization identity (identity to
|
|
// login as), followed by a US-ASCII NUL character, followed by the
|
|
// authentication identity (identity whose password will be used),
|
|
// followed by a US-ASCII NUL character, followed by the clear-text
|
|
// password. The client may leave the authorization identity empty to
|
|
// indicate that it is the same as the authentication identity.
|
|
|
|
// Fast PLAIN authentication process:
|
|
// Client sends "AUTH PLAIN " plus Base64-encoded string: identity\0username\0password
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
missingNUL := base64.StdEncoding.EncodeToString([]byte("valid\x00password"))
|
|
valid := base64.StdEncoding.EncodeToString([]byte("identity\x00valid\x00password"))
|
|
invalid := base64.StdEncoding.EncodeToString([]byte("identity\x00invalid\x00password"))
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+invalidBase64, "501")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+missingNUL, "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+invalid, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+valid, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHPLAINFastAndEmpty(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN without TLS in use must return 504 unrecognised type.
|
|
cmdCode(t, conn, "AUTH PLAIN", "504")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH PLAIN with TLS in use can proceed.
|
|
// RFC 2595 specifies:
|
|
// The client sends the authorization identity (identity to
|
|
// login as), followed by a US-ASCII NUL character, followed by the
|
|
// authentication identity (identity whose password will be used),
|
|
// followed by a US-ASCII NUL character, followed by the clear-text
|
|
// password. The client may leave the authorization identity empty to
|
|
// indicate that it is the same as the authentication identity.
|
|
|
|
// Fast PLAIN authentication process with empty authorisation identity:
|
|
// Client sends "AUTH PLAIN " plus Base64-encoded string: \0username\0password
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
missingNUL := base64.StdEncoding.EncodeToString([]byte("valid\x00password"))
|
|
valid := base64.StdEncoding.EncodeToString([]byte("\x00valid\x00password"))
|
|
invalid := base64.StdEncoding.EncodeToString([]byte("\x00invalid\x00password"))
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+invalidBase64, "501")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+missingNUL, "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+invalid, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
cmdCode(t, tlsConn, "AUTH PLAIN "+valid, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
// makeCRAMMD5Response is a helper function to create the CRAM-MD5 hash.
|
|
func makeCRAMMD5Response(challenge string, username string, secret string) (string, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(challenge)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash := hmac.New(md5.New, []byte(secret))
|
|
hash.Write(decoded)
|
|
buffer := make([]byte, 0, hash.Size())
|
|
response := fmt.Sprintf("%s %x", username, hash.Sum(buffer))
|
|
return base64.StdEncoding.EncodeToString([]byte(response)), nil
|
|
}
|
|
|
|
func TestCmdAUTHCRAMMD5(t *testing.T) {
|
|
server := &Server{AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH CRAM-MD5 without TLS in use can proceed.
|
|
// RFC 2195 specifies:
|
|
// The challenge format is that of a Message-ID email header value.
|
|
// Challenge format: '<' + random digits + '.' + timestamp in digits + '@' + fully-qualified server hostname + '>'
|
|
// Challenge example: <1896.697170952@postoffice.reston.mci.net>
|
|
// The response format consists of the username, a space and a digest.
|
|
// Digest calculation: MD5((secret XOR opad), MD5((secret XOR ipad), challenge))
|
|
// Response example: tim b913a602c7eda7a495b4e6e7334d3890
|
|
|
|
// CRAM-MD5 authentication process:
|
|
// Client sends "AUTH CRAM-MD5".
|
|
// Server sends "334 " plus Base64-encoded challenge.
|
|
// Client sends Base64-encoded response.
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
cmdCode(t, conn, invalidBase64, "501")
|
|
|
|
// Test valid credentials with missing space (causing a parse error).
|
|
line := cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
valid, _ := makeCRAMMD5Response(line[4:], "valid", "password")
|
|
buffer, _ := base64.StdEncoding.DecodeString(valid)
|
|
buffer = bytes.Replace(buffer, []byte(" "), []byte(""), 1)
|
|
missingSpace := base64.StdEncoding.EncodeToString(buffer)
|
|
cmdCode(t, conn, string(missingSpace), "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
line = cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
invalid, err := makeCRAMMD5Response(line[4:], "invalid", "password")
|
|
if err != nil {
|
|
cmdCode(t, conn, "*", "501")
|
|
}
|
|
cmdCode(t, conn, invalid, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
line = cmdCode(t, conn, "AUTH CRAM-MD5", "334")
|
|
valid, err = makeCRAMMD5Response(line[4:], "valid", "password")
|
|
if err != nil {
|
|
cmdCode(t, conn, "*", "501")
|
|
}
|
|
cmdCode(t, conn, valid, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, conn, "AUTH LOGIN", "503")
|
|
cmdCode(t, conn, "AUTH PLAIN", "503")
|
|
cmdCode(t, conn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestCmdAUTHCRAMMD5WithTLS(t *testing.T) {
|
|
server := &Server{TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, AuthHandler: testAuthHandler}
|
|
conn := newConn(t, server)
|
|
cmdCode(t, conn, "EHLO host.example.com", "250")
|
|
|
|
// Upgrade to TLS.
|
|
cmdCode(t, conn, "STARTTLS", "220")
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
err := tlsConn.Handshake()
|
|
if err != nil {
|
|
t.Errorf("Failed to perform TLS handshake")
|
|
}
|
|
cmdCode(t, tlsConn, "EHLO host.example.com", "250")
|
|
|
|
// AUTH CRAM-MD5 with TLS in use can proceed.
|
|
// RFC 2195 specifies:
|
|
// The challenge format is that of a Message-ID email header value.
|
|
// Challenge format: '<' + random digits + '.' + timestamp in digits + '@' + fully-qualified server hostname + '>'
|
|
// Challenge example: <1896.697170952@postoffice.reston.mci.net>
|
|
// The response format consists of the username, a space and a digest.
|
|
// Digest calculation: MD5((secret XOR opad), MD5((secret XOR ipad), challenge))
|
|
// Response example: tim b913a602c7eda7a495b4e6e7334d3890
|
|
|
|
// CRAM-MD5 authentication process:
|
|
// Client sends "AUTH CRAM-MD5".
|
|
// Server sends "334 " plus Base64-encoded challenge.
|
|
// Client sends Base64-encoded response.
|
|
invalidBase64 := "==" // Invalid Base64 string.
|
|
|
|
// Corrupt credentials must return 501 syntax error.
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "334")
|
|
cmdCode(t, tlsConn, invalidBase64, "501")
|
|
|
|
// Test valid credentials with missing space (causing a parse error).
|
|
line := cmdCode(t, tlsConn, "AUTH CRAM-MD5", "334")
|
|
valid, _ := makeCRAMMD5Response(line[4:], "valid", "password")
|
|
buffer, _ := base64.StdEncoding.DecodeString(valid)
|
|
buffer = bytes.Replace(buffer, []byte(" "), []byte(""), 1)
|
|
missingSpace := base64.StdEncoding.EncodeToString(buffer)
|
|
cmdCode(t, tlsConn, string(missingSpace), "501")
|
|
|
|
// Invalid credentials must return 535 authentication credentials invalid.
|
|
line = cmdCode(t, tlsConn, "AUTH CRAM-MD5", "334")
|
|
invalid, err := makeCRAMMD5Response(line[4:], "invalid", "password")
|
|
if err != nil {
|
|
cmdCode(t, tlsConn, "*", "501")
|
|
}
|
|
cmdCode(t, tlsConn, invalid, "535")
|
|
|
|
// Valid credentials must return 235 authentication succeeded.
|
|
line = cmdCode(t, tlsConn, "AUTH CRAM-MD5", "334")
|
|
valid, err = makeCRAMMD5Response(line[4:], "valid", "password")
|
|
if err != nil {
|
|
cmdCode(t, tlsConn, "*", "501")
|
|
}
|
|
cmdCode(t, tlsConn, valid, "235")
|
|
|
|
// AUTH after prior successful AUTH must return 503 bad sequence.
|
|
cmdCode(t, tlsConn, "AUTH LOGIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH PLAIN", "503")
|
|
cmdCode(t, tlsConn, "AUTH CRAM-MD5", "503")
|
|
|
|
cmdCode(t, tlsConn, "QUIT", "221")
|
|
tlsConn.Close()
|
|
}
|
|
|
|
// Benchmark the mail handling without the network stack introducing latency.
|
|
func BenchmarkReceive(b *testing.B) {
|
|
server := &Server{} // Default server configuration.
|
|
clientConn, serverConn := net.Pipe()
|
|
session := server.newSession(serverConn)
|
|
go session.serve()
|
|
|
|
reader := bufio.NewReader(clientConn)
|
|
_, _ = reader.ReadString('\n') // Read greeting message first.
|
|
|
|
b.ResetTimer()
|
|
|
|
// Benchmark a full mail transaction.
|
|
for i := 0; i < b.N; i++ {
|
|
fmt.Fprintf(clientConn, "%s\r\n", "HELO host.example.com")
|
|
_, _ = reader.ReadString('\n')
|
|
fmt.Fprintf(clientConn, "%s\r\n", "MAIL FROM:<sender@example.com>")
|
|
_, _ = reader.ReadString('\n')
|
|
fmt.Fprintf(clientConn, "%s\r\n", "RCPT TO:<recipient@example.com>")
|
|
_, _ = reader.ReadString('\n')
|
|
fmt.Fprintf(clientConn, "%s\r\n", "DATA")
|
|
_, _ = reader.ReadString('\n')
|
|
fmt.Fprintf(clientConn, "%s\r\n", "Test message.\r\n.")
|
|
_, _ = reader.ReadString('\n')
|
|
fmt.Fprintf(clientConn, "%s\r\n", "QUIT")
|
|
_, _ = reader.ReadString('\n')
|
|
}
|
|
}
|
|
|
|
func TestCmdShutdown(t *testing.T) {
|
|
|
|
srv := &Server{}
|
|
|
|
conn := newConn(t, srv)
|
|
|
|
// Send HELO, expect greeting.
|
|
cmdCode(t, conn, "HELO host.example.com", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
cmdCode(t, conn, "HELO host.example.com", "250")
|
|
cmdCode(t, conn, "DATA", "503")
|
|
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
t.Errorf("Error shutting down server: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// give the shutdown time to act
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// shutdown will wait until the end of the session
|
|
cmdCode(t, conn, "HELO host.example.com", "250")
|
|
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "250")
|
|
cmdCode(t, conn, "RCPT TO:<recipient@example.com>", "250")
|
|
|
|
// this will trigger the close
|
|
cmdCode(t, conn, "QUIT", "221")
|
|
|
|
// connection should now be closed
|
|
fmt.Fprintf(conn, "%s\r\n", "HELO host.example.com")
|
|
_, err := bufio.NewReader(conn).ReadString('\n')
|
|
if err != io.EOF {
|
|
t.Errorf("Expected connection to be closed\n")
|
|
}
|
|
|
|
conn.Close()
|
|
}
|