Compare commits
24 commits
Author | SHA1 | Date | |
---|---|---|---|
194daa594d | |||
71409bf7cd | |||
fb16a3fc81 | |||
33c7d9338e | |||
e4e75cf975 | |||
|
006a4f9d6d | ||
|
737dbc490e | ||
|
166abaf187 | ||
|
b5f17a69f6 | ||
|
32be721d71 | ||
|
7c73bd1d49 | ||
|
74ef92e4ba | ||
|
c6661acb7e | ||
|
724b678a1a | ||
|
d975e4449b | ||
|
9c93a62f1a | ||
|
6323615a6b | ||
|
21709bf51f | ||
|
52710f748c | ||
|
0002b7c334 | ||
|
e7d1686eae | ||
|
9fccea2351 | ||
|
0857b2e4ed | ||
|
f70a0c8d52 |
17 changed files with 716 additions and 237 deletions
34
.github/workflows/go.yml
vendored
Normal file
34
.github/workflows/go.yml
vendored
Normal 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
8
Makefile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
test:
|
||||||
|
go test .
|
||||||
|
|
||||||
|
dkim-proxy:
|
||||||
|
cd _examples && go get . && go build .
|
||||||
|
|
||||||
|
testsum:
|
||||||
|
gotestsum --format testname
|
|
@ -13,6 +13,11 @@ Features
|
||||||
* Configurable limits for: connection count, message size and recipient count
|
* Configurable limits for: connection count, message size and recipient count
|
||||||
* Hands incoming e-mail off to a configured callback function
|
* Hands incoming e-mail off to a configured callback function
|
||||||
|
|
||||||
|
Version numbers
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The package is tagged with semantic version numbers, making it suitable for use in a [Go Module](https://github.com/golang/go/wiki/Modules).
|
||||||
|
|
||||||
Feedback
|
Feedback
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
5
_examples/dkim-proxy/README.md
Normal file
5
_examples/dkim-proxy/README.md
Normal 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.
|
5
_examples/dkim-proxy/go.mod
Normal file
5
_examples/dkim-proxy/go.mod
Normal 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
|
|
@ -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)
|
||||||
}
|
}
|
30
envelope.go
30
envelope.go
|
@ -3,7 +3,7 @@ package smtpd
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,28 +20,42 @@ 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",
|
||||||
}
|
}
|
||||||
|
|
||||||
if peer.TLS != nil {
|
if peer.TLS != nil {
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
if val, ok := tlsVersions[peer.TLS.Version]; ok {
|
||||||
|
version = val
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher := tls.CipherSuiteName(peer.TLS.CipherSuite)
|
||||||
|
|
||||||
tlsDetails = fmt.Sprintf(
|
tlsDetails = fmt.Sprintf(
|
||||||
"\r\n\t(version=%s cipher=0x%x);",
|
"\r\n\t(version=%s cipher=%s);",
|
||||||
tlsVersions[peer.TLS.Version],
|
version,
|
||||||
peer.TLS.CipherSuite,
|
cipher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerIP := ""
|
||||||
|
if addr, ok := peer.Addr.(*net.TCPAddr); ok {
|
||||||
|
peerIP = addr.IP.String()
|
||||||
|
}
|
||||||
|
|
||||||
line := wrap([]byte(fmt.Sprintf(
|
line := wrap([]byte(fmt.Sprintf(
|
||||||
"Received: from %s [%s] by %s with %s;%s\r\n\t%s\r\n",
|
"Received: from %s ([%s]) by %s with %s;%s\r\n\t%s\r\n",
|
||||||
peer.HeloName,
|
peer.HeloName,
|
||||||
strings.Split(peer.Addr.String(), ":")[0],
|
peerIP,
|
||||||
peer.ServerName,
|
peer.ServerName,
|
||||||
peer.Protocol,
|
peer.Protocol,
|
||||||
tlsDetails,
|
tlsDetails,
|
||||||
time.Now().Format("Mon Jan 2 15:04:05 -0700 2006"),
|
time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700 (MST)"),
|
||||||
)))
|
)))
|
||||||
|
|
||||||
env.Data = append(env.Data, line...)
|
env.Data = append(env.Data, line...)
|
||||||
|
|
22
error.go
Normal file
22
error.go
Normal 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")
|
||||||
|
|
||||||
|
|
|
@ -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
4
go.mod
|
@ -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
28
onceCloseListerner.go
Normal 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) }
|
17
protocol.go
17
protocol.go
|
@ -7,7 +7,6 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -36,11 +35,11 @@ func parseLine(line string) (cmd command) {
|
||||||
// Account for some clients breaking the standard and having
|
// Account for some clients breaking the standard and having
|
||||||
// an extra whitespace after the ':' character. Example:
|
// an extra whitespace after the ':' character. Example:
|
||||||
//
|
//
|
||||||
// MAIL FROM: <christian@technobabble.dk>
|
// MAIL FROM: <test@example.org>
|
||||||
//
|
//
|
||||||
// Should be:
|
// Should be:
|
||||||
//
|
//
|
||||||
// MAIL FROM:<christian@technobabble.dk>
|
// MAIL FROM:<test@example.org>
|
||||||
//
|
//
|
||||||
// Thus, we add a check if the second field ends with ':'
|
// Thus, we add a check if the second field ends with ':'
|
||||||
// and appends the rest of the third field.
|
// and appends the rest of the third field.
|
||||||
|
@ -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
|
||||||
|
|
|
@ -34,7 +34,7 @@ func TestParseLine(t *testing.T) {
|
||||||
t.Fatalf("unexpected params: %v", cmd.params)
|
t.Fatalf("unexpected params: %v", cmd.params)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = parseLine("MAIL FROM:<christian@technobabble.dk>")
|
cmd = parseLine("MAIL FROM:<test@example.org>")
|
||||||
if cmd.action != "MAIL" {
|
if cmd.action != "MAIL" {
|
||||||
t.Fatalf("unexpected action: %s", cmd.action)
|
t.Fatalf("unexpected action: %s", cmd.action)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func TestParseLine(t *testing.T) {
|
||||||
t.Fatalf("unexpected value for param 0: %v", cmd.params[0])
|
t.Fatalf("unexpected value for param 0: %v", cmd.params[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.params[1] != "<christian@technobabble.dk>" {
|
if cmd.params[1] != "<test@example.org>" {
|
||||||
t.Fatalf("unexpected value for param 1: %v", cmd.params[1])
|
t.Fatalf("unexpected value for param 1: %v", cmd.params[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ func TestParseLine(t *testing.T) {
|
||||||
|
|
||||||
func TestParseLineMailformedMAILFROM(t *testing.T) {
|
func TestParseLineMailformedMAILFROM(t *testing.T) {
|
||||||
|
|
||||||
cmd := parseLine("MAIL FROM: <christian@technobabble.dk>")
|
cmd := parseLine("MAIL FROM: <test@example.org>")
|
||||||
if cmd.action != "MAIL" {
|
if cmd.action != "MAIL" {
|
||||||
t.Fatalf("unexpected action: %s", cmd.action)
|
t.Fatalf("unexpected action: %s", cmd.action)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func TestParseLineMailformedMAILFROM(t *testing.T) {
|
||||||
t.Fatalf("unexpected value for param 0: %v", cmd.params[0])
|
t.Fatalf("unexpected value for param 0: %v", cmd.params[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.params[1] != "<christian@technobabble.dk>" {
|
if cmd.params[1] != "<test@example.org>" {
|
||||||
t.Fatalf("unexpected value for param 1: %v", cmd.params[1])
|
t.Fatalf("unexpected value for param 1: %v", cmd.params[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
162
session.go
Normal file
162
session.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
266
smtpd.go
266
smtpd.go
|
@ -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{
|
||||||
|
@ -119,6 +105,9 @@ func (srv *Server) newSession(c net.Conn) (s *session) {
|
||||||
tlsConn, s.tls = c.(*tls.Conn)
|
tlsConn, s.tls = c.(*tls.Conn)
|
||||||
|
|
||||||
if s.tls {
|
if s.tls {
|
||||||
|
// run handshake otherwise it's done when we first
|
||||||
|
// read/write and connection state will be invalid
|
||||||
|
tlsConn.Handshake()
|
||||||
state := tlsConn.ConnectionState()
|
state := tlsConn.ConnectionState()
|
||||||
s.peer.TLS = &state
|
s.peer.TLS = &state
|
||||||
}
|
}
|
||||||
|
@ -131,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()
|
||||||
|
|
||||||
|
@ -144,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
|
||||||
}
|
}
|
||||||
|
@ -170,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()
|
||||||
|
@ -179,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 {
|
||||||
|
@ -228,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()
|
|
||||||
}
|
|
||||||
|
|
354
smtpd_test.go
354
smtpd_test.go
|
@ -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()
|
||||||
|
@ -943,12 +1070,13 @@ func TestEnvelopeReceived(t *testing.T) {
|
||||||
Hostname: "foobar.example.net",
|
Hostname: "foobar.example.net",
|
||||||
Handler: func(peer smtpd.Peer, env smtpd.Envelope) error {
|
Handler: func(peer smtpd.Peer, env smtpd.Envelope) error {
|
||||||
env.AddReceivedLine(peer)
|
env.AddReceivedLine(peer)
|
||||||
if !bytes.HasPrefix(env.Data, []byte("Received: from localhost [127.0.0.1] by foobar.example.net with ESMTP;")) {
|
if !bytes.HasPrefix(env.Data, []byte("Received: from localhost ([127.0.0.1]) by foobar.example.net with ESMTP;")) {
|
||||||
t.Fatal("Wrong received line.")
|
t.Fatal("Wrong received line.")
|
||||||
}
|
}
|
||||||
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()
|
||||||
|
|
||||||
|
@ -1002,7 +1132,7 @@ func TestHELO(t *testing.T) {
|
||||||
t.Fatalf("Dial failed: %v", err)
|
t.Fatalf("Dial failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd(c.Text, 502, "MAIL FROM:<christian@technobabble.dk>"); err != nil {
|
if err := cmd(c.Text, 502, "MAIL FROM:<test@example.org>"); err != nil {
|
||||||
t.Fatalf("MAIL before HELO didn't fail: %v", err)
|
t.Fatalf("MAIL before HELO didn't fail: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1010,7 +1140,7 @@ func TestHELO(t *testing.T) {
|
||||||
t.Fatalf("HELO failed: %v", err)
|
t.Fatalf("HELO failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd(c.Text, 250, "MAIL FROM:<christian@technobabble.dk>"); err != nil {
|
if err := cmd(c.Text, 250, "MAIL FROM:<test@example.org>"); err != nil {
|
||||||
t.Fatalf("MAIL after HELO failed: %v", err)
|
t.Fatalf("MAIL after HELO failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -1119,7 +1254,7 @@ func TestNoBracketsSender(t *testing.T) {
|
||||||
t.Fatalf("HELO failed: %v", err)
|
t.Fatalf("HELO failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd(c.Text, 250, "MAIL FROM:christian@technobabble.dk"); err != nil {
|
if err := cmd(c.Text, 250, "MAIL FROM:test@example.org"); err != nil {
|
||||||
t.Fatalf("MAIL without brackets failed: %v", err)
|
t.Fatalf("MAIL without brackets failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -1211,11 +1351,12 @@ func TestMailformedMAILFROM(t *testing.T) {
|
||||||
|
|
||||||
addr, closer := runserver(t, &smtpd.Server{
|
addr, closer := runserver(t, &smtpd.Server{
|
||||||
SenderChecker: func(peer smtpd.Peer, addr string) error {
|
SenderChecker: func(peer smtpd.Peer, addr string) error {
|
||||||
if addr != "christian@technobabble.dk" {
|
if addr != "test@example.org" {
|
||||||
return smtpd.Error{Code: 502, Message: "Denied"}
|
return smtpd.Error{Code: 502, Message: "Denied"}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
ProtocolLogger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
|
||||||
})
|
})
|
||||||
|
|
||||||
defer closer()
|
defer closer()
|
||||||
|
@ -1229,7 +1370,7 @@ func TestMailformedMAILFROM(t *testing.T) {
|
||||||
t.Fatalf("HELO failed: %v", err)
|
t.Fatalf("HELO failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd(c.Text, 250, "MAIL FROM: <christian@technobabble.dk>"); err != nil {
|
if err := cmd(c.Text, 250, "MAIL FROM: <test@example.org>"); err != nil {
|
||||||
t.Fatalf("MAIL FROM failed with extra whitespace: %v", err)
|
t.Fatalf("MAIL FROM failed with extra whitespace: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue