136 lines
4.1 KiB
Go
136 lines
4.1 KiB
Go
package outbound
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"io"
|
|
"net"
|
|
"net/mail"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/example/notify-gateway/internal/config"
|
|
"github.com/example/notify-gateway/internal/testutil"
|
|
)
|
|
|
|
func smtpFixture(t *testing.T, mode string, rcptCode int) (config.OutboundConfig, *tls.Config, <-chan []byte) {
|
|
t.Helper()
|
|
serverTLS, clientTLS := testutil.TLSConfigs()
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { listener.Close() })
|
|
received := make(chan []byte, 1)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
raw, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer raw.Close()
|
|
var conn net.Conn = raw
|
|
if mode == "tls" {
|
|
conn = tls.Server(raw, serverTLS)
|
|
}
|
|
tp := textproto.NewConn(conn)
|
|
tp.PrintfLine("220 test SMTP")
|
|
for {
|
|
line, err := tp.ReadLine()
|
|
if err != nil {
|
|
return
|
|
}
|
|
verb := strings.Split(line, " ")[0]
|
|
switch verb {
|
|
case "EHLO":
|
|
if mode == "starttls" {
|
|
tp.PrintfLine("250-test\r\n250-STARTTLS\r\n250 AUTH PLAIN")
|
|
} else {
|
|
tp.PrintfLine("250-test\r\n250 AUTH PLAIN")
|
|
}
|
|
case "STARTTLS":
|
|
tp.PrintfLine("220 upgrade")
|
|
conn = tls.Server(raw, serverTLS)
|
|
tp = textproto.NewConn(conn)
|
|
case "AUTH":
|
|
data, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(line, "AUTH PLAIN "))
|
|
if string(data) != "\x00user\x00password" {
|
|
t.Error("unexpected SMTP auth")
|
|
}
|
|
tp.PrintfLine("235 OK")
|
|
case "MAIL":
|
|
tp.PrintfLine("250 OK")
|
|
case "RCPT":
|
|
tp.PrintfLine("%d recipient", rcptCode)
|
|
case "DATA":
|
|
tp.PrintfLine("354 data")
|
|
b, err := tp.ReadDotBytes()
|
|
if err != nil {
|
|
return
|
|
}
|
|
received <- b
|
|
tp.PrintfLine("250 accepted")
|
|
case "QUIT":
|
|
return // A lost QUIT response must still count as success.
|
|
default:
|
|
tp.PrintfLine("500 unsupported")
|
|
}
|
|
}
|
|
}()
|
|
t.Cleanup(func() { listener.Close(); <-done })
|
|
host, port, _ := net.SplitHostPort(listener.Addr().String())
|
|
p, _ := strconv.Atoi(port)
|
|
return config.OutboundConfig{ID: "mail", Provider: "smtp", SMTPHost: host, SMTPPort: p, TLSMode: mode, Username: "user", Password: "password", From: "sender@example.org", To: []string{"to@example.org"}, Live: true, TimeoutS: 2}, clientTLS, received
|
|
}
|
|
|
|
func TestSMTPEncryptedDeliveryAndMIME(t *testing.T) {
|
|
for _, mode := range []string{"tls", "starttls"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
o, tlsConfig, received := smtpFixture(t, mode, 250)
|
|
r, err := sendSMTPWithTLS(context.Background(), o, map[string]any{"title": "Grüße\r\nBcc: bad@example.org", "message": "Hallo Welt äöü"}, "stable-id", tlsConfig)
|
|
if err != nil || r.StatusCode != 250 {
|
|
t.Fatalf("%+v %v", r, err)
|
|
}
|
|
msg, err := mail.ReadMessage(strings.NewReader(string(<-received)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Header.Get("Bcc") != "" || msg.Header.Get("Message-ID") != "<stable-id@notify-gateway.local>" || msg.Header.Get("Auto-Submitted") != "auto-generated" {
|
|
t.Fatal("unsafe/missing headers")
|
|
}
|
|
body, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, msg.Body))
|
|
if err != nil || string(body) != "Hallo Welt äöü" {
|
|
t.Fatalf("body=%s %v", body, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestSMTPFailures(t *testing.T) {
|
|
for _, code := range []int{450, 550} {
|
|
t.Run(strconv.Itoa(code), func(t *testing.T) {
|
|
o, tlsConfig, _ := smtpFixture(t, "tls", code)
|
|
r, err := sendSMTPWithTLS(context.Background(), o, map[string]any{"message": "test"}, "id", tlsConfig)
|
|
if err == nil || r.StatusCode != code || r.Retryable != (code == 450) {
|
|
t.Fatalf("%+v %v", r, err)
|
|
}
|
|
})
|
|
}
|
|
t.Run("untrusted certificate", func(t *testing.T) {
|
|
o, _, _ := smtpFixture(t, "tls", 250)
|
|
if _, err := sendSMTP(context.Background(), o, map[string]any{"message": "test"}, "id"); err == nil {
|
|
t.Fatal("untrusted certificate accepted")
|
|
}
|
|
})
|
|
t.Run("no STARTTLS", func(t *testing.T) {
|
|
o, tlsConfig, _ := smtpFixture(t, "plain", 250)
|
|
o.TLSMode = "starttls"
|
|
if _, err := sendSMTPWithTLS(context.Background(), o, map[string]any{"message": "test"}, "id", tlsConfig); err == nil {
|
|
t.Fatal("plaintext SMTP accepted")
|
|
}
|
|
})
|
|
}
|