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.
This commit is contained in:
MHSanaei
2026-11-01 13:18:54 +01:00
parent 06cb1742a1
commit 8bbfda0510
2 changed files with 101 additions and 20 deletions
@@ -1,6 +1,9 @@
package email
import (
"bufio"
"bytes"
"errors"
"net"
"testing"
"time"
@@ -44,3 +47,49 @@ func TestSendPlainReturnsOnStalledServer(t *testing.T) {
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)
}
})
}
}