package smtpserver import ( "net/smtp" "testing" "mailgoserver/internal/db" "mailgoserver/internal/mailstore" ) // createTestMailboxWithQuota mirrors newTestBackendWithMailbox's inline mailbox setup, // parameterized by quota so this file can create both a normal and an // effectively-always-full mailbox. func createTestMailboxWithQuota(t *testing.T, backend *Backend, store *mailstore.Store, email string, quotaBytes int64) int64 { t.Helper() dek := mailstore.GenerateDEK() wrapped, nonce, err := store.WrapDEK(dek) if err != nil { t.Fatal(err) } hash, err := db.HashPassword("portal-password-unused") if err != nil { t.Fatal(err) } mailboxID, err := backend.DB.CreateMailbox(email, hash, 1, quotaBytes, wrapped, nonce) if err != nil { t.Fatal(err) } return mailboxID } // TestPartialLocalDeliveryFailureBouncesAndAccepts confirms a multi-recipient // transaction where one local mailbox accepts the message and another can't (quota // exceeded, discovered only during DATA — RCPT can't catch it) is accepted (250, not // 550 — the successful recipient already has it, so the client must not retry the // whole transaction) and that the sender gets a bounce in their own mailbox describing // the recipient that failed. func TestPartialLocalDeliveryFailureBouncesAndAccepts(t *testing.T) { backend, okMailboxID := newTestBackendWithMailbox(t) store := backend.Mailstore fullMailboxID := createTestMailboxWithQuota(t, backend, store, "full@example.com", 1) senderMailboxID := createTestMailboxWithQuota(t, backend, store, "test@example.com", 5*1024*1024*1024) addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil { t.Fatalf("auth: %v", err) } if err := c.Mail("test@example.com"); err != nil { t.Fatalf("MAIL FROM: %v", err) } if err := c.Rcpt("inbox@example.com"); err != nil { t.Fatalf("RCPT (ok mailbox): %v", err) } if err := c.Rcpt("full@example.com"); err != nil { t.Fatalf("RCPT (over-quota mailbox, still accepted at RCPT time): %v", err) } w, err := c.Data() if err != nil { t.Fatal(err) } if _, err := w.Write([]byte("Subject: hello\r\n\r\nhi there")); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatalf("expected DATA to succeed (250, partial success) despite one recipient failing, got: %v", err) } okMsgs, err := backend.DB.ListMessagesInFolder(okMailboxID, "INBOX") if err != nil { t.Fatal(err) } if len(okMsgs) != 1 { t.Fatalf("expected the message delivered to inbox@example.com, got %d messages", len(okMsgs)) } fullMsgs, err := backend.DB.ListMessagesInFolder(fullMailboxID, "INBOX") if err != nil { t.Fatal(err) } if len(fullMsgs) != 0 { t.Fatalf("expected no message delivered to the over-quota mailbox, got %d", len(fullMsgs)) } bounces, err := backend.DB.ListMessagesInFolder(senderMailboxID, "INBOX") if err != nil { t.Fatal(err) } if len(bounces) != 1 { t.Fatalf("expected 1 bounce message in the sender's own mailbox, got %d", len(bounces)) } if bounces[0].CachedSubject != "Undelivered Mail Returned to Sender" { t.Errorf("bounce subject = %q", bounces[0].CachedSubject) } }