Compare commits

..

15 commits

Author SHA1 Message Date
194daa594d
more refractoring, adding makefile for tests 2025-03-14 12:46:30 +01:00
71409bf7cd
slight refractoring 2025-03-14 12:13:06 +01:00
fb16a3fc81
moved from log.Logger to slog.Logger 2025-03-14 12:07:44 +01:00
33c7d9338e
re-added SSL3.0 to envelope 2025-03-13 15:07:09 +01:00
e4e75cf975
cleaned up deprecations and warnings 2025-03-13 14:40:39 +01:00
Christian Joergensen
006a4f9d6d
Cleanup actions. (#15) 2022-09-30 12:51:37 -04:00
Christian Joergensen
737dbc490e
Run tests daily. (#14) 2022-09-27 20:24:12 -04:00
Alex SZAKALY
166abaf187
chore: add leading underscore to examples directory (#13)
Unfortunately the dependency github.com/eaigner/dkim
is no longer available

As a temporary solution we need to put leading underscore
to let Go recursively ignore it

Signed-off-by: Alex Szakaly <alex.szakaly@gmail.com>
2021-11-22 00:56:59 +01:00
Jonathon Reinhart
b5f17a69f6
Implement graceful shutdown (#11)
* Implement graceful shutdown

This borrows heavily from the implementation of net/http Server.Shutdown().

Closes #10

* Add Server.Address() function to return listener address of a Server

This is useful for logging, etc.

* Refactor out Server.Wait() and add 'wait' param to Shutdown()

This allows a consumer which runs multiple Servers (decke/smtprelay) to
first shut down all servers (passing wait=false) so no new connections are
accepted, and then wait for outstanding client connections to finish.
2021-03-29 10:24:55 +02:00
Bernhard Fröhlich
32be721d71
Verify that user is properly authenticated before sending mail if AUTH is required (#6)
* Verify that user is properly authenticated before sending mail if AUTH is required

* Add testcase to verify that user is properly authenticated before sending mail if authenticator is setup

* Fix TestErrors() to not misuse auth bypass
2020-06-07 18:48:25 +02:00
Christian Joergensen
7c73bd1d49
Add go.yml (#8) 2020-05-23 10:26:20 +02:00
Christian Joergensen
74ef92e4ba
Merge pull request #5 from decke/received-header-fixes
Use new tls.CipherSuiteName() from Go 1.14 instead of own map
2020-05-21 18:49:32 +02:00
Christian Joergensen
c6661acb7e
Merge pull request #7 from chrj/ssl-testing
New test SSL certificate, better logging in tests
2020-05-21 18:44:49 +02:00
Christian Joergensen
724b678a1a New test certificate. Better logging in tests. 2020-05-21 18:44:04 +02:00
Bernhard Froehlich
d975e4449b
Use new tls.CipherSuiteName() from Go 1.14 instead of own outdated map and add tls.VersionTLS13 to version list 2020-05-11 12:36:56 +00:00
15 changed files with 677 additions and 249 deletions

34
.github/workflows/go.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: smtpd
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '12 0 * * *'
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out the code
uses: actions/checkout@v2
- name: Get dependencies
run: go get -v -t -d ./...
- name: Build
run: go build -v .
- name: Test
run: go test -v .

8
Makefile Normal file
View file

@ -0,0 +1,8 @@
test:
go test .
dkim-proxy:
cd _examples && go get . && go build .
testsum:
gotestsum --format testname

View file

@ -0,0 +1,5 @@
# smtpd dkim-proxy
## Important Note
The dependency `github.com/eaigner/dkim` is no longer available thus the example can not be built.

View file

@ -0,0 +1,5 @@
module github.com/chrj/smtpd/_examples/dkim-proxy
go 1.14
require github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb

View file

@ -4,7 +4,6 @@ package main
import ( import (
"bytes" "bytes"
"flag" "flag"
"io/ioutil"
"log" "log"
"net/smtp" "net/smtp"
@ -60,7 +59,7 @@ func main() {
log.Fatalf("DKIM configuration error: %v", err) log.Fatalf("DKIM configuration error: %v", err)
} }
privKey, err = ioutil.ReadFile(*privKeyFile) privKey, err = io.ReadFile(*privKeyFile)
if err != nil { if err != nil {
log.Fatalf("Couldn't read private key: %v", err) log.Fatalf("Couldn't read private key: %v", err)
} }

View file

@ -20,36 +20,11 @@ func (env *Envelope) AddReceivedLine(peer Peer) {
tlsDetails := "" tlsDetails := ""
tlsVersions := map[uint16]string{ tlsVersions := map[uint16]string{
tls.VersionSSL30: "SSL3.0", 0x300: "SSL3.0",
tls.VersionTLS10: "TLS1.0", tls.VersionTLS10: "TLS1.0",
tls.VersionTLS11: "TLS1.1", tls.VersionTLS11: "TLS1.1",
tls.VersionTLS12: "TLS1.2", tls.VersionTLS12: "TLS1.2",
} tls.VersionTLS13: "TLS1.3",
tlsCiphers := map[uint16]string{
tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA256: "TLS_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
tls.TLS_FALLBACK_SCSV: "TLS_FALLBACK_SCSV",
} }
if peer.TLS != nil { if peer.TLS != nil {
@ -59,11 +34,7 @@ func (env *Envelope) AddReceivedLine(peer Peer) {
version = val version = val
} }
cipher := fmt.Sprintf("0x%x", peer.TLS.CipherSuite) cipher := tls.CipherSuiteName(peer.TLS.CipherSuite)
if val, ok := tlsCiphers[peer.TLS.CipherSuite]; ok {
cipher = val
}
tlsDetails = fmt.Sprintf( tlsDetails = fmt.Sprintf(
"\r\n\t(version=%s cipher=%s);", "\r\n\t(version=%s cipher=%s);",

22
error.go Normal file
View file

@ -0,0 +1,22 @@
package smtpd
import (
"errors"
"fmt"
)
// Error represents an Error reported in the SMTP session.
type Error struct {
Code int // The integer error code
Message string // The error message
}
// Error returns a string representation of the SMTP error
func (e Error) Error() string {
return fmt.Sprintf("%d %s", e.Code, e.Message)
}
// ErrServerClosed is returned by the Server's Serve and ListenAndServe,
// methods after a call to Shutdown.
var ErrServerClosed = errors.New("smtp: Server closed")

View file

@ -5,7 +5,7 @@ import (
"net/smtp" "net/smtp"
"strings" "strings"
"github.com/chrj/smtpd" "git.jmbit.de/jmb/smtpd"
) )
func ExampleServer() { func ExampleServer() {

4
go.mod
View file

@ -1,3 +1,3 @@
module github.com/chrj/smtpd module git.jmbit.de/jmb/smtpd
require github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb go 1.24

28
onceCloseListerner.go Normal file
View file

@ -0,0 +1,28 @@
package smtpd
import (
"net"
"sync"
"sync/atomic"
)
// onceCloseListener wraps a net.Listener, protecting it from
// multiple Close calls.
type onceCloseListener struct {
net.Listener
once sync.Once
closeErr error
}
func (oc *onceCloseListener) Close() error {
oc.once.Do(oc.close)
return oc.closeErr
}
func (oc *onceCloseListener) close() { oc.closeErr = oc.Listener.Close() }
type atomicBool int32
func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) }
func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) }

View file

@ -7,7 +7,6 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/textproto" "net/textproto"
"strconv" "strconv"
@ -202,6 +201,11 @@ func (session *session) handleMAIL(cmd command) {
return return
} }
if session.server.Authenticator != nil && session.peer.Username == "" {
session.reply(530, "Authentication Required.")
return
}
if !session.tls && session.server.ForceTLS { if !session.tls && session.server.ForceTLS {
session.reply(502, "Please turn on TLS by issuing a STARTTLS command.") session.reply(502, "Please turn on TLS by issuing a STARTTLS command.")
return return
@ -283,6 +287,7 @@ func (session *session) handleRCPT(cmd command) {
} }
func (session *session) handleSTARTTLS(cmd command) { func (session *session) handleSTARTTLS(cmd command) {
_ = cmd
if session.tls { if session.tls {
session.reply(502, "Already running in TLS") session.reply(502, "Already running in TLS")
@ -329,6 +334,7 @@ func (session *session) handleSTARTTLS(cmd command) {
} }
func (session *session) handleDATA(cmd command) { func (session *session) handleDATA(cmd command) {
_ = cmd
if session.envelope == nil || len(session.envelope.Recipients) == 0 { if session.envelope == nil || len(session.envelope.Recipients) == 0 {
session.reply(502, "Missing RCPT TO command.") session.reply(502, "Missing RCPT TO command.")
@ -366,7 +372,7 @@ func (session *session) handleDATA(cmd command) {
} }
// Discard the rest and report an error. // Discard the rest and report an error.
_, err = io.Copy(ioutil.Discard, reader) _, err = io.Copy(io.Discard, reader)
if err != nil { if err != nil {
// Network error, ignore // Network error, ignore
@ -385,17 +391,20 @@ func (session *session) handleDATA(cmd command) {
} }
func (session *session) handleRSET(cmd command) { func (session *session) handleRSET(cmd command) {
_ = cmd
session.reset() session.reset()
session.reply(250, "Go ahead") session.reply(250, "Go ahead")
return return
} }
func (session *session) handleNOOP(cmd command) { func (session *session) handleNOOP(cmd command) {
_ = cmd
session.reply(250, "Go ahead") session.reply(250, "Go ahead")
return return
} }
func (session *session) handleQUIT(cmd command) { func (session *session) handleQUIT(cmd command) {
_ = cmd
session.reply(221, "OK, bye") session.reply(221, "OK, bye")
session.close() session.close()
return return

162
session.go Normal file
View file

@ -0,0 +1,162 @@
package smtpd
import (
"net"
"bufio"
"strings"
"fmt"
"time"
)
type session struct {
server *Server
peer Peer
envelope *Envelope
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
scanner *bufio.Scanner
tls bool
}
func (session *session) serve() {
defer session.close()
if !session.server.EnableProxyProtocol {
session.welcome()
}
for {
for session.scanner.Scan() {
line := session.scanner.Text()
session.logf("received: %s", strings.TrimSpace(line))
session.handle(line)
}
err := session.scanner.Err()
if err == bufio.ErrTooLong {
session.reply(500, "Line too long")
// Advance reader to the next newline
session.reader.ReadString('\n')
session.scanner = bufio.NewScanner(session.reader)
// Reset and have the client start over.
session.reset()
continue
}
break
}
}
func (session *session) reject() {
session.reply(421, "Too busy. Try again later.")
session.close()
}
func (session *session) reset() {
session.envelope = nil
}
func (session *session) welcome() {
if session.server.ConnectionChecker != nil {
err := session.server.ConnectionChecker(session.peer)
if err != nil {
session.error(err)
session.close()
return
}
}
session.reply(220, session.server.WelcomeMessage)
}
func (session *session) reply(code int, message string) {
session.logf("sending: %d %s", code, message)
fmt.Fprintf(session.writer, "%d %s\r\n", code, message)
session.flush()
}
func (session *session) flush() {
session.conn.SetWriteDeadline(time.Now().Add(session.server.WriteTimeout))
session.writer.Flush()
session.conn.SetReadDeadline(time.Now().Add(session.server.ReadTimeout))
}
func (session *session) error(err error) {
if smtpdError, ok := err.(Error); ok {
session.reply(smtpdError.Code, smtpdError.Message)
} else {
session.reply(502, fmt.Sprintf("%s", err))
}
}
func (session *session) logf(format string, v ...interface{}) {
if session.server.ProtocolLogger == nil {
return
}
session.server.ProtocolLogger.Info(fmt.Sprintf(
"%s [peer:%s]",
fmt.Sprintf(format, v...),
session.peer.Addr,
))
}
func (session *session) logError(err error, desc string) {
session.server.ProtocolLogger.Error(desc, "error", err)
}
func (session *session) extensions() []string {
extensions := []string{
fmt.Sprintf("SIZE %d", session.server.MaxMessageSize),
"8BITMIME",
"PIPELINING",
}
if session.server.EnableXCLIENT {
extensions = append(extensions, "XCLIENT")
}
if session.server.TLSConfig != nil && !session.tls {
extensions = append(extensions, "STARTTLS")
}
if session.server.Authenticator != nil && session.tls {
extensions = append(extensions, "AUTH PLAIN LOGIN")
}
return extensions
}
func (session *session) deliver() error {
if session.server.Handler != nil {
return session.server.Handler(session.peer, *session.envelope)
}
return nil
}
func (session *session) close() {
session.writer.Flush()
time.Sleep(200 * time.Millisecond)
session.conn.Close()
}

263
smtpd.go
View file

@ -4,10 +4,12 @@ package smtpd
import ( import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"log" "log/slog"
"log"
"net" "net"
"strings" "sync"
"time" "time"
) )
@ -48,7 +50,15 @@ 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.
ProtocolLogger *log.Logger ProtocolLogger *slog.Logger
// mu guards doneChan and makes closing it and listener atomic from
// perspective of Serve()
mu sync.Mutex
doneChan chan struct{}
listener *net.Listener
waitgrp sync.WaitGroup
inShutdown atomicBool // true when server is in shutdown
} }
// Protocol represents the protocol used in the SMTP session // Protocol represents the protocol used in the SMTP session
@ -73,30 +83,6 @@ type Peer struct {
TLS *tls.ConnectionState // TLS Connection details, if on TLS TLS *tls.ConnectionState // TLS Connection details, if on TLS
} }
// Error represents an Error reported in the SMTP session.
type Error struct {
Code int // The integer error code
Message string // The error message
}
// Error returns a string representation of the SMTP error
func (e Error) Error() string { return fmt.Sprintf("%d %s", e.Code, e.Message) }
type session struct {
server *Server
peer Peer
envelope *Envelope
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
scanner *bufio.Scanner
tls bool
}
func (srv *Server) newSession(c net.Conn) (s *session) { func (srv *Server) newSession(c net.Conn) (s *session) {
s = &session{ s = &session{
@ -134,6 +120,9 @@ func (srv *Server) newSession(c net.Conn) (s *session) {
// ListenAndServe starts the SMTP server and listens on the address provided // ListenAndServe starts the SMTP server and listens on the address provided
func (srv *Server) ListenAndServe(addr string) error { func (srv *Server) ListenAndServe(addr string) error {
if srv.shuttingDown() {
return ErrServerClosed
}
srv.configureDefaults() srv.configureDefaults()
@ -147,24 +136,32 @@ func (srv *Server) ListenAndServe(addr string) error {
// Serve starts the SMTP server and listens on the Listener provided // Serve starts the SMTP server and listens on the Listener provided
func (srv *Server) Serve(l net.Listener) error { func (srv *Server) Serve(l net.Listener) error {
if srv.shuttingDown() {
return ErrServerClosed
}
srv.configureDefaults() srv.configureDefaults()
l = &onceCloseListener{Listener: l}
defer l.Close() defer l.Close()
srv.listener = &l
var limiter chan struct{} var limiter chan struct{}
if srv.MaxConnections > 0 { if srv.MaxConnections > 0 {
limiter = make(chan struct{}, srv.MaxConnections) limiter = make(chan struct{}, srv.MaxConnections)
} else {
limiter = nil
} }
for { for {
conn, e := l.Accept() conn, e := l.Accept()
if e != nil { if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() { select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := e.(net.Error); ok && ne.Timeout() {
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
} }
@ -173,8 +170,10 @@ func (srv *Server) Serve(l net.Listener) error {
session := srv.newSession(conn) session := srv.newSession(conn)
if limiter != nil { srv.waitgrp.Add(1)
go func() { go func() {
defer srv.waitgrp.Done()
if limiter != nil {
select { select {
case limiter <- struct{}{}: case limiter <- struct{}{}:
session.serve() session.serve()
@ -182,15 +181,53 @@ func (srv *Server) Serve(l net.Listener) error {
default: default:
session.reject() session.reject()
} }
}() } else {
} else { session.serve()
go session.serve() }
} }()
} }
} }
// Shutdown instructs the server to shutdown, starting by closing the
// associated listener. If wait is true, it will wait for the shutdown
// to complete. If wait is false, Wait must be called afterwards.
func (srv *Server) Shutdown(wait bool) error {
var lnerr error
srv.inShutdown.setTrue()
// First close the listener
srv.mu.Lock()
if srv.listener != nil {
lnerr = (*srv.listener).Close();
}
srv.closeDoneChanLocked()
srv.mu.Unlock()
// Now wait for all client connections to close
if wait {
srv.Wait()
}
return lnerr
}
// Wait waits for all client connections to close and the server to finish
// shutting down.
func (srv *Server) Wait() error {
if !srv.shuttingDown() {
return errors.New("Server has not been Shutdown")
}
srv.waitgrp.Wait()
return nil
}
// Address returns the listening address of the server
func (srv *Server) Address() net.Addr {
return (*srv.listener).Addr();
}
func (srv *Server) configureDefaults() { func (srv *Server) configureDefaults() {
if srv.MaxMessageSize == 0 { if srv.MaxMessageSize == 0 {
@ -231,138 +268,34 @@ func (srv *Server) configureDefaults() {
} }
func (session *session) serve() { // From net/http/server.go
defer session.close() func (s *Server) shuttingDown() bool {
return s.inShutdown.isSet()
}
if !session.server.EnableProxyProtocol { func (s *Server) getDoneChan() <-chan struct{} {
session.welcome() s.mu.Lock()
defer s.mu.Unlock()
return s.getDoneChanLocked()
}
func (s *Server) getDoneChanLocked() chan struct{} {
if s.doneChan == nil {
s.doneChan = make(chan struct{})
} }
return s.doneChan
for {
for session.scanner.Scan() {
line := session.scanner.Text()
session.logf("received: %s", strings.TrimSpace(line))
session.handle(line)
}
err := session.scanner.Err()
if err == bufio.ErrTooLong {
session.reply(500, "Line too long")
// Advance reader to the next newline
session.reader.ReadString('\n')
session.scanner = bufio.NewScanner(session.reader)
// Reset and have the client start over.
session.reset()
continue
}
break
}
} }
func (session *session) reject() { func (s *Server) closeDoneChanLocked() {
session.reply(421, "Too busy. Try again later.") ch := s.getDoneChanLocked()
session.close() select {
} case <-ch:
// Already closed. Don't close again.
func (session *session) reset() { default:
session.envelope = nil // Safe to close here. We're the only closer, guarded
} // by s.mu.
close(ch)
func (session *session) welcome() {
if session.server.ConnectionChecker != nil {
err := session.server.ConnectionChecker(session.peer)
if err != nil {
session.error(err)
session.close()
return
}
}
session.reply(220, session.server.WelcomeMessage)
}
func (session *session) reply(code int, message string) {
session.logf("sending: %d %s", code, message)
fmt.Fprintf(session.writer, "%d %s\r\n", code, message)
session.flush()
}
func (session *session) flush() {
session.conn.SetWriteDeadline(time.Now().Add(session.server.WriteTimeout))
session.writer.Flush()
session.conn.SetReadDeadline(time.Now().Add(session.server.ReadTimeout))
}
func (session *session) error(err error) {
if smtpdError, ok := err.(Error); ok {
session.reply(smtpdError.Code, smtpdError.Message)
} else {
session.reply(502, fmt.Sprintf("%s", err))
} }
} }
func (session *session) logf(format string, v ...interface{}) {
if session.server.ProtocolLogger == nil {
return
}
session.server.ProtocolLogger.Output(2, fmt.Sprintf(
"%s [peer:%s]",
fmt.Sprintf(format, v...),
session.peer.Addr,
))
}
func (session *session) logError(err error, desc string) {
session.logf("%s: %v ", desc, err)
}
func (session *session) extensions() []string {
extensions := []string{
fmt.Sprintf("SIZE %d", session.server.MaxMessageSize),
"8BITMIME",
"PIPELINING",
}
if session.server.EnableXCLIENT {
extensions = append(extensions, "XCLIENT")
}
if session.server.TLSConfig != nil && !session.tls {
extensions = append(extensions, "STARTTLS")
}
if session.server.Authenticator != nil && session.tls {
extensions = append(extensions, "AUTH PLAIN LOGIN")
}
return extensions
}
func (session *session) deliver() error {
if session.server.Handler != nil {
return session.server.Handler(session.peer, *session.envelope)
}
return nil
}
func (session *session) close() {
session.writer.Flush()
time.Sleep(200 * time.Millisecond)
session.conn.Close()
}

View file

@ -5,37 +5,103 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/smtp" "net/smtp"
"net/textproto" "net/textproto"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/chrj/smtpd" "git.jmbit.de/jmb/smtpd"
) )
var localhostCert = []byte(`-----BEGIN CERTIFICATE----- var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIIBkzCCAT+gAwIBAgIQf4LO8+QzcbXRHJUo6MvX7zALBgkqhkiG9w0BAQswEjEQ MIIFkzCCA3ugAwIBAgIUQvhoyGmvPHq8q6BHrygu4dPp0CkwDQYJKoZIhvcNAQEL
MA4GA1UEChMHQWNtZSBDbzAeFw03MDAxMDEwMDAwMDBaFw04MTA1MjkxNjAwMDBa BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
MBIxEDAOBgNVBAoTB0FjbWUgQ28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAx2Uj GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
2nl0ESnMMrdUOwQnpnIPQzQBX9MIYT87VxhHzImOukWcq5DrmN1ZB//diyrgiCLv DTIwMDUyMTE2MzI1NVoXDTMwMDUxOTE2MzI1NVowWTELMAkGA1UEBhMCQVUxEzAR
D0udX3YXNHMn1Ki8awIDAQABo3MwcTAOBgNVHQ8BAf8EBAMCAKQwEwYDVR0lBAww BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
CgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zA5BgNVHREEMjAwggtleGFtcGxl IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
LmNvbYIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAsGCSqGSIb3 MIICCgKCAgEAk773plyfK4u2uIIZ6H7vEnTb5qJT6R/KCY9yniRvCFV+jCrISAs9
DQEBCwNBAGcaB2Il0TIXFcJOdOLGPa6F8qZH1ZHBtVlCBnaJn4vZJGzID+V36Gn0 0pgU+/P8iePnZRGbRCGGt1B+1/JAVLIYFZuawILHNs4yWKAwh0uNpR1Pec8v7vpq
hA1AYfGAaF0c43oQofvv+XqQlTe4a+M= NpdUzXKQKIqFynSkcLA8c2DOZwuhwVc8rZw50yY3r4i4Vxf0AARGXapnBfy6WerR
/6xT7y/OcK8+8aOirDQ9P6WlvZ0ynZKi5q2o1eEVypT2us9r+HsCYosKEEAnjzjJ
wP5rvredxUqb7OupIkgA4Nq80+4tqGGQfWetmoi3zXRhKpijKjgxBOYEqSUWm9ws
/aC91Iy5RawyTB0W064z75OgfuI5GwFUbyLD0YVN4DLSAI79GUfvc8NeLEXpQvYq
+f8P+O1Hbv2AQ28IdbyQrNefB+/WgjeTvXLploNlUihVhpmLpptqnauw/DY5Ix51
w60lHIZ6esNOmMQB+/z/IY5gpmuo66yH8aSCPSYBFxQebB7NMqYGOS9nXx62/Bn1
OUVXtdtrhfbbdQW6zMZjka0t8m83fnGw3ISyBK2NNnSzOgycu0ChsW6sk7lKyeWa
85eJGsQWIhkOeF9v9GAIH/qsrgVpToVC9Krbk+/gqYIYF330tHQrzp6M6LiG5OY1
P7grUBovN2ZFt10B97HxWKa2f/8t9sfHZuKbfLSFbDsyI2JyNDh+Vk0CAwEAAaNT
MFEwHQYDVR0OBBYEFOLdIQUr3gDQF5YBor75mlnCdKngMB8GA1UdIwQYMBaAFOLd
IQUr3gDQF5YBor75mlnCdKngMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggIBAGddhQMVMZ14TY7bU8CMuc9IrXUwxp59QfqpcXCA2pHc2VOWkylv2dH7
ta6KooPMKwJ61d+coYPK1zMUvNHHJCYVpVK0r+IGzs8mzg91JJpX2gV5moJqNXvd
Fy6heQJuAvzbb0Tfsv8KN7U8zg/ovpS7MbY+8mRJTQINn2pCzt2y2C7EftLK36x0
KeBWqyXofBJoMy03VfCRqQlWK7VPqxluAbkH+bzji1g/BTkoCKzOitAbjS5lT3sk
oCrF9N6AcjpFOH2ZZmTO4cZ6TSWfrb/9OWFXl0TNR9+x5c/bUEKoGeSMV1YT1SlK
TNFMUlq0sPRgaITotRdcptc045M6KF777QVbrYm/VH1T3pwPGYu2kUdYHcteyX9P
8aRG4xsPGQ6DD7YjBFsif2fxlR3nQ+J/l/+eXHO4C+eRbxi15Z2NjwVjYpxZlUOq
HD96v516JkMJ63awbY+HkYdEUBKqR55tzcvNWnnfiboVmIecjAjoV4zStwDIti9u
14IgdqqAbnx0ALbUWnvfFloLdCzPPQhgLHpTeRSEDPljJWX8rmy8iQtRb0FWYQ3z
A2wsUyutzK19nt4hjVrTX0At9ku3gMmViXFlbvyA1Y4TuhdUYqJauMBrWKl2ybDW
yhdKg/V3yTwgBUtb3QO4m1khNQjQLuPFVxULGEA38Y5dXSONsYnt
-----END CERTIFICATE-----`) -----END CERTIFICATE-----`)
var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- var localhostKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIBPAIBAAJBAMdlI9p5dBEpzDK3VDsEJ6ZyD0M0AV/TCGE/O1cYR8yJjrpFnKuQ MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCTvvemXJ8ri7a4
65jdWQf/3Ysq4Igi7w9LnV92FzRzJ9SovGsCAwEAAQJAVaFw2VWJbAmIQUuMJ+Ar ghnofu8SdNvmolPpH8oJj3KeJG8IVX6MKshICz3SmBT78/yJ4+dlEZtEIYa3UH7X
6wZW2aSO5okpsyHFqSyrQQIcAj/QOq8P83F8J10IreFWNlBlywJU9c7IlJtn/lqq 8kBUshgVm5rAgsc2zjJYoDCHS42lHU95zy/u+mo2l1TNcpAoioXKdKRwsDxzYM5n
AQIhAOxHXOxrKPxqTIdIcNnWye/HRQ+5VD54QQr1+M77+bEBAiEA2AmsNNqj2fKj C6HBVzytnDnTJjeviLhXF/QABEZdqmcF/LpZ6tH/rFPvL85wrz7xo6KsND0/paW9
j2xk+4vnBSY0vrb4q/O3WZ46oorawWsCIQDWdpfzx/i11E6OZMR6FinJSNh4w0Gi nTKdkqLmrajV4RXKlPa6z2v4ewJiiwoQQCePOMnA/mu+t53FSpvs66kiSADg2rzT
SkjPiCBE0BX+AQIhAI/TiLk7YmBkQG3ovSYW0vvDntPlXpKj08ovJFw4U0D3AiEA 7i2oYZB9Z62aiLfNdGEqmKMqODEE5gSpJRab3Cz9oL3UjLlFrDJMHRbTrjPvk6B+
lGjGna4oaauI0CWI6pG0wg4zklTnrDWK7w9h/S/T4e0= 4jkbAVRvIsPRhU3gMtIAjv0ZR+9zw14sRelC9ir5/w/47Udu/YBDbwh1vJCs158H
-----END RSA PRIVATE KEY-----`) 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
hBgHM6A0WJC9MO3aAKRBcp48y6DXNA==
-----END PRIVATE KEY-----`)
func cmd(c *textproto.Conn, expectedCode int, format string, args ...interface{}) error { func cmd(c *textproto.Conn, expectedCode int, format string, args ...interface{}) error {
id, err := c.Cmd(format, args...) id, err := c.Cmd(format, args...)
@ -91,7 +157,9 @@ func runsslserver(t *testing.T, server *smtpd.Server) (addr string, closer func(
func TestSMTP(t *testing.T) { func TestSMTP(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
c, err := smtp.Dial(addr) c, err := smtp.Dial(addr)
@ -164,7 +232,9 @@ func TestListenAndServe(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{})
closer() closer()
server := &smtpd.Server{} server := &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}
go func() { go func() {
server.ListenAndServe(addr) server.ListenAndServe(addr)
@ -186,8 +256,9 @@ func TestListenAndServe(t *testing.T) {
func TestSTARTTLS(t *testing.T) { func TestSTARTTLS(t *testing.T) {
addr, closer := runsslserver(t, &smtpd.Server{ addr, closer := runsslserver(t, &smtpd.Server{
Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, Authenticator: func(peer smtpd.Peer, username, password string) error { return nil },
ForceTLS: true, ForceTLS: true,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -275,7 +346,8 @@ func TestAuthRejection(t *testing.T) {
Authenticator: func(peer smtpd.Peer, username, password string) error { Authenticator: func(peer smtpd.Peer, username, password string) error {
return smtpd.Error{Code: 550, Message: "Denied"} return smtpd.Error{Code: 550, Message: "Denied"}
}, },
ForceTLS: true, ForceTLS: true,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -298,7 +370,8 @@ func TestAuthRejection(t *testing.T) {
func TestAuthNotSupported(t *testing.T) { func TestAuthNotSupported(t *testing.T) {
addr, closer := runsslserver(t, &smtpd.Server{ addr, closer := runsslserver(t, &smtpd.Server{
ForceTLS: true, ForceTLS: true,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -318,12 +391,40 @@ func TestAuthNotSupported(t *testing.T) {
} }
func TestAuthBypass(t *testing.T) {
addr, closer := runsslserver(t, &smtpd.Server{
Authenticator: func(peer smtpd.Peer, username, password string) error {
return smtpd.Error{Code: 550, Message: "Denied"}
},
ForceTLS: true,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer()
c, err := smtp.Dial(addr)
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
if err := c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil {
t.Fatalf("STARTTLS failed: %v", err)
}
if err := c.Mail("sender@example.org"); err == nil {
t.Fatal("Unexpected MAIL success")
}
}
func TestConnectionCheck(t *testing.T) { func TestConnectionCheck(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{ addr, closer := runserver(t, &smtpd.Server{
ConnectionChecker: func(peer smtpd.Peer) error { ConnectionChecker: func(peer smtpd.Peer) error {
return smtpd.Error{Code: 552, Message: "Denied"} return smtpd.Error{Code: 552, Message: "Denied"}
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -340,6 +441,7 @@ func TestConnectionCheckSimpleError(t *testing.T) {
ConnectionChecker: func(peer smtpd.Peer) error { ConnectionChecker: func(peer smtpd.Peer) error {
return errors.New("Denied") return errors.New("Denied")
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -359,6 +461,7 @@ func TestHELOCheck(t *testing.T) {
} }
return smtpd.Error{Code: 552, Message: "Denied"} return smtpd.Error{Code: 552, Message: "Denied"}
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -380,6 +483,7 @@ func TestSenderCheck(t *testing.T) {
SenderChecker: func(peer smtpd.Peer, addr string) error { SenderChecker: func(peer smtpd.Peer, addr string) error {
return smtpd.Error{Code: 552, Message: "Denied"} return smtpd.Error{Code: 552, Message: "Denied"}
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -401,6 +505,7 @@ func TestRecipientCheck(t *testing.T) {
RecipientChecker: func(peer smtpd.Peer, addr string) error { RecipientChecker: func(peer smtpd.Peer, addr string) error {
return smtpd.Error{Code: 552, Message: "Denied"} return smtpd.Error{Code: 552, Message: "Denied"}
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -424,6 +529,7 @@ func TestMaxMessageSize(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{ addr, closer := runserver(t, &smtpd.Server{
MaxMessageSize: 5, MaxMessageSize: 5,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -480,6 +586,7 @@ func TestHandler(t *testing.T) {
} }
return nil return nil
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -524,6 +631,7 @@ func TestRejectHandler(t *testing.T) {
Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { Handler: func(peer smtpd.Peer, env smtpd.Envelope) error {
return smtpd.Error{Code: 550, Message: "Rejected"} return smtpd.Error{Code: 550, Message: "Rejected"}
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -566,6 +674,7 @@ func TestMaxConnections(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{ addr, closer := runserver(t, &smtpd.Server{
MaxConnections: 1, MaxConnections: 1,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -587,6 +696,7 @@ func TestNoMaxConnections(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{ addr, closer := runserver(t, &smtpd.Server{
MaxConnections: -1, MaxConnections: -1,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -602,7 +712,8 @@ func TestNoMaxConnections(t *testing.T) {
func TestMaxRecipients(t *testing.T) { func TestMaxRecipients(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{ addr, closer := runserver(t, &smtpd.Server{
MaxRecipients: 1, MaxRecipients: 1,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -632,7 +743,9 @@ func TestMaxRecipients(t *testing.T) {
func TestInvalidHelo(t *testing.T) { func TestInvalidHelo(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -649,7 +762,9 @@ func TestInvalidHelo(t *testing.T) {
func TestInvalidSender(t *testing.T) { func TestInvalidSender(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -666,7 +781,9 @@ func TestInvalidSender(t *testing.T) {
func TestInvalidRecipient(t *testing.T) { func TestInvalidRecipient(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -687,7 +804,9 @@ func TestInvalidRecipient(t *testing.T) {
func TestRCPTbeforeMAIL(t *testing.T) { func TestRCPTbeforeMAIL(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -704,7 +823,9 @@ func TestRCPTbeforeMAIL(t *testing.T) {
func TestDATAbeforeRCPT(t *testing.T) { func TestDATAbeforeRCPT(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -734,6 +855,7 @@ func TestInterruptedDATA(t *testing.T) {
t.Fatal("Accepted DATA despite disconnection") t.Fatal("Accepted DATA despite disconnection")
return nil return nil
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -771,6 +893,7 @@ func TestTimeoutClose(t *testing.T) {
MaxConnections: 1, MaxConnections: 1,
ReadTimeout: time.Second, ReadTimeout: time.Second,
WriteTimeout: time.Second, WriteTimeout: time.Second,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -805,8 +928,9 @@ func TestTimeoutClose(t *testing.T) {
func TestTLSTimeout(t *testing.T) { func TestTLSTimeout(t *testing.T) {
addr, closer := runsslserver(t, &smtpd.Server{ addr, closer := runsslserver(t, &smtpd.Server{
ReadTimeout: time.Second * 2, ReadTimeout: time.Second * 2,
WriteTimeout: time.Second * 2, WriteTimeout: time.Second * 2,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -848,7 +972,9 @@ func TestTLSTimeout(t *testing.T) {
func TestLongLine(t *testing.T) { func TestLongLine(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -886,6 +1012,7 @@ func TestXCLIENT(t *testing.T) {
} }
return nil return nil
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -948,7 +1075,8 @@ func TestEnvelopeReceived(t *testing.T) {
} }
return nil return nil
}, },
ForceTLS: true, ForceTLS: true,
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -993,7 +1121,9 @@ func TestEnvelopeReceived(t *testing.T) {
func TestHELO(t *testing.T) { func TestHELO(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -1027,7 +1157,8 @@ func TestHELO(t *testing.T) {
func TestLOGINAuth(t *testing.T) { func TestLOGINAuth(t *testing.T) {
addr, closer := runsslserver(t, &smtpd.Server{ addr, closer := runsslserver(t, &smtpd.Server{
Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, Authenticator: func(peer smtpd.Peer, username, password string) error { return nil },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -1081,7 +1212,9 @@ func TestLOGINAuth(t *testing.T) {
func TestNullSender(t *testing.T) { func TestNullSender(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -1106,7 +1239,9 @@ func TestNullSender(t *testing.T) {
func TestNoBracketsSender(t *testing.T) { func TestNoBracketsSender(t *testing.T) {
addr, closer := runserver(t, &smtpd.Server{}) addr, closer := runserver(t, &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
})
defer closer() defer closer()
@ -1137,7 +1272,8 @@ func TestErrors(t *testing.T) {
} }
server := &smtpd.Server{ server := &smtpd.Server{
Authenticator: func(peer smtpd.Peer, username, password string) error { return nil }, Authenticator: func(peer smtpd.Peer, username, password string) error { return nil },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
} }
addr, closer := runserver(t, server) addr, closer := runserver(t, server)
@ -1161,12 +1297,8 @@ func TestErrors(t *testing.T) {
t.Fatalf("AUTH didn't fail: %v", err) t.Fatalf("AUTH didn't fail: %v", err)
} }
if err := c.Mail("sender@example.org"); err != nil {
t.Fatalf("MAIL failed: %v", err)
}
if err := c.Mail("sender@example.org"); err == nil { if err := c.Mail("sender@example.org"); err == nil {
t.Fatal("Duplicate MAIL didn't fail") t.Fatalf("MAIL didn't fail")
} }
if err := cmd(c.Text, 502, "STARTTLS"); err != nil { if err := cmd(c.Text, 502, "STARTTLS"); err != nil {
@ -1201,6 +1333,14 @@ func TestErrors(t *testing.T) {
t.Fatalf("AUTH didn't work: %v", err) t.Fatalf("AUTH didn't work: %v", err)
} }
if err := c.Mail("sender@example.org"); err != nil {
t.Fatalf("MAIL failed: %v", err)
}
if err := c.Mail("sender@example.org"); err == nil {
t.Fatalf("Duplicate MAIL didn't fail")
}
if err := c.Quit(); err != nil { if err := c.Quit(); err != nil {
t.Fatalf("Quit failed: %v", err) t.Fatalf("Quit failed: %v", err)
} }
@ -1216,6 +1356,7 @@ func TestMailformedMAILFROM(t *testing.T) {
} }
return nil return nil
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}) })
defer closer() defer closer()
@ -1261,6 +1402,7 @@ func TestTLSListener(t *testing.T) {
} }
return nil return nil
}, },
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
} }
go func() { go func() {
@ -1294,3 +1436,113 @@ func TestTLSListener(t *testing.T) {
} }
} }
func TestShutdown(t *testing.T) {
fmt.Println("Starting test")
server := &smtpd.Server{
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen failed: %v", err)
}
srvres := make(chan error)
go func() {
t.Log("Starting server")
srvres <- server.Serve(ln)
}()
// Connect a client
c, err := smtp.Dial(ln.Addr().String())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
if err := c.Hello("localhost"); err != nil {
t.Fatalf("HELO failed: %v", err)
}
// While the client connection is open, shut down the server (without
// waiting for it to finish)
err = server.Shutdown(false)
if err != nil {
t.Fatalf("Shutdown returned error: %v", err)
}
// Verify that Shutdown() worked by attempting to connect another client
_, err = smtp.Dial(ln.Addr().String())
if err == nil {
t.Fatalf("Dial did not fail as expected")
}
if _, typok := err.(*net.OpError); !typok {
t.Fatalf("Dial did not return net.OpError as expected: %v (%T)", err, err)
}
// Wait for shutdown to complete
shutres := make(chan error)
go func() {
t.Log("Waiting for server shutdown to finish")
shutres <- server.Wait()
}()
// Slight delay to ensure Shutdown() blocks
time.Sleep(250 * time.Millisecond)
// Wait() should not have returned yet due to open client conn
select {
case shuterr := <-shutres:
t.Fatalf("Wait() returned early w/ error: %v", shuterr)
default:
}
// Now close the client
t.Log("Closing client connection")
if err := c.Quit(); err != nil {
t.Fatalf("QUIT failed: %v", err)
}
c.Close()
// Wait for Wait() to return
t.Log("Waiting for Wait() to return")
select {
case shuterr := <-shutres:
if shuterr != nil {
t.Fatalf("Wait() returned error: %v", shuterr)
}
case <-time.After(15 * time.Second):
t.Fatalf("Timed out waiting for Wait() to return")
}
// Wait for Serve() to return
t.Log("Waiting for Serve() to return")
select {
case srverr := <-srvres:
if srverr != smtpd.ErrServerClosed {
t.Fatalf("Serve() returned error: %v", srverr)
}
case <-time.After(15 * time.Second):
t.Fatalf("Timed out waiting for Serve() to return")
}
}
func TestServeFailsIfShutdown(t *testing.T) {
server := &smtpd.Server{}
err := server.Shutdown(true)
if err != nil {
t.Fatalf("Shutdown() failed: %v", err)
}
err = server.Serve(nil)
if err != smtpd.ErrServerClosed {
t.Fatalf("Serve() did not return ErrServerClosed: %v", err)
}
}
func TestWaitFailsIfNotShutdown(t *testing.T) {
server := &smtpd.Server{}
err := server.Wait()
if err == nil {
t.Fatalf("Wait() did not fail as expected")
}
}