Files
3x-ui/internal/web/service/email/email_deadline_test.go
T
MHSanaei 8bbfda0510 fix(email): cap what one SMTP session reads from the server
net/smtp buffers a server reply of any length, so a mail server, or
anyone on the path before STARTTLS, could send a greeting or EHLO reply
that never ends and grow the panel's heap until the 30 s deadline, on
every notification.

Each session now reads at most 1 MiB from the server, below TLS, and
fails with a clear error past it.
2026-11-01 13:18:54 +01:00

96 lines
2.3 KiB
Go

package email
import (
"bufio"
"bytes"
"errors"
"net"
"testing"
"time"
)
func TestSendPlainReturnsOnStalledServer(t *testing.T) {
orig := smtpDeadline
smtpDeadline = 300 * time.Millisecond
t.Cleanup(func() { smtpDeadline = orig })
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
stall := make(chan struct{})
defer close(stall)
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
<-stall
}()
s := &EmailService{}
done := make(chan error, 1)
go func() {
done <- s.sendPlain(ln.Addr().String(), nil, "from@example.com",
[]string{"to@example.com"}, []byte("body"), "example.com")
}()
select {
case err := <-done:
if err == nil {
t.Fatal("expected an error from a silent SMTP server, got nil")
}
case <-time.After(3 * time.Second):
t.Fatal("sendPlain did not return on a stalled server within the deadline")
}
}
// The flood is capped well above smtpMaxServerBytes: without the cap the client
// buffers it all and then fails on the closed socket, not errSMTPReplyTooLarge.
func TestSendPlainStopsReadingOversizedReply(t *testing.T) {
tests := []struct {
name string
greeting string
flood []byte
}{
{"greeting line that never ends", "220 ", bytes.Repeat([]byte("a"), 64<<10)},
{"EHLO reply that never ends", "220 ready\r\n", bytes.Repeat([]byte("250-X\r\n"), 8<<10)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
_, _ = conn.Write([]byte(tt.greeting))
if tt.greeting[len(tt.greeting)-1] == '\n' {
_, _ = bufio.NewReader(conn).ReadString('\n')
}
for sent := 0; sent < 16<<20; sent += len(tt.flood) {
if _, err := conn.Write(tt.flood); err != nil {
return
}
}
}()
s := &EmailService{}
err = s.sendPlain(ln.Addr().String(), nil, "from@example.com",
[]string{"to@example.com"}, []byte("body"), "example.com")
if !errors.Is(err, errSMTPReplyTooLarge) {
t.Fatalf("sendPlain error = %v, want %v", err, errSMTPReplyTooLarge)
}
})
}
}