added JMAP (not tested yet)

This commit is contained in:
2026-08-22 15:03:32 +01:00
parent 123e58a6b0
commit c34c876c99
9 changed files with 408 additions and 23 deletions
+79
View File
@@ -1,11 +1,15 @@
package relay
import (
"io"
"net"
"os"
"strconv"
"testing"
"time"
"github.com/emersion/go-smtp"
"mailgoserver/internal/db"
"mailgoserver/internal/toolbox"
)
@@ -190,3 +194,78 @@ func TestProcessQueueOnceExhaustsRetriesAndBounces(t *testing.T) {
t.Errorf("expected email_log status 'failed', got %q", log.Status)
}
}
// rejectingBackend is a stand-in "receiving MTA" that rejects every RCPT with a fixed
// SMTP status/message — used to prove a permanent (5xx) rejection bounces on the first
// attempt instead of working through retrySchedule.
type rejectingBackend struct{ code int }
func (b *rejectingBackend) NewSession(*smtp.Conn) (smtp.Session, error) {
return &rejectingSession{code: b.code}, nil
}
type rejectingSession struct{ code int }
func (s *rejectingSession) Mail(from string, opts *smtp.MailOptions) error { return nil }
func (s *rejectingSession) Rcpt(to string, opts *smtp.RcptOptions) error {
return &smtp.SMTPError{Code: s.code, EnhancedCode: smtp.NoEnhancedCode, Message: "poor reputation"}
}
func (s *rejectingSession) Data(r io.Reader) error { _, err := io.ReadAll(r); return err }
func (s *rejectingSession) Reset() {}
func (s *rejectingSession) Logout() error { return nil }
func TestProcessQueueOnceBouncesImmediatelyOnPermanentRejection(t *testing.T) {
database := testDB(t)
s := smtp.NewServer(&rejectingBackend{code: 554})
s.Domain = "mx.example.com"
s.AllowInsecureAuth = true
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close() })
go s.Serve(l)
_, portStr, _ := net.SplitHostPort(l.Addr().String())
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatal(err)
}
r := &Relay{
DB: database, Hostname: "sender.example.com", Timeout: 5 * time.Second,
Logger: toolbox.GetLogger("relay_test"), port: port, mxLookup: fixedMXLookup,
}
logID, err := database.InsertEmailLog(db.EmailLog{MailFrom: "from@example.com", ToAddress: "to@dest.example", Status: "queued"})
if err != nil {
t.Fatal(err)
}
if err := database.InsertEmailRecipientLog(db.EmailRecipientLog{EmailLogID: logID, Recipient: "to@dest.example", RecipientType: "to", Status: "queued"}); err != nil {
t.Fatal(err)
}
if err := r.EnqueueForDelivery(logID, "from@example.com", []string{"to@dest.example"}, []string{"to"}, "Subject: hi\r\n\r\nbody"); err != nil {
t.Fatal(err)
}
// One tick. If this were misclassified as a transient failure it would still be
// pending (rescheduled per retrySchedule), not gone.
r.ProcessQueueOnce(5, 10)
pending, err := database.CountPendingRelayQueueItemsForEmailLog(logID)
if err != nil {
t.Fatal(err)
}
if pending != 0 {
t.Fatalf("expected the queue row to be gone after a single permanent (5xx) rejection, got %d pending", pending)
}
recipients, err := database.ListRecipientLogsForEmail(logID)
if err != nil {
t.Fatal(err)
}
if recipients[0].Status != "failed" {
t.Errorf("expected recipient status 'failed' immediately on a 554, got %q", recipients[0].Status)
}
if recipients[0].ErrorMessage == "" {
t.Error("expected the recipient log to carry the server's rejection reason")
}
}