The review of d254a99..fd29468 (read-only, a separate agent) found all five fixes sound and nothing above low. Fixed here: - The M1 e2e step passed on the unfixed code: the stranger's RSET between MAIL FROMs ends the transaction, Postfix's mail_reset() calls milter_abort(), and the old code released the slot too. The stranger now keeps one MAIL FROM open (strangerHoldsMailFrom) while the domain's own application sends; under the old code that slot was the domain's one. - A domain import could still store a report address on the server's own name (W1's form rule only): checkDomainImport refuses one that is not the domain's own hosted address. Not a mailbox either way, but the panel would show an address the server refuses. Cases in TestDomainImportIsCheckedLikeTheForms. - DeleteDomain matches the reports' domain without regard to case. - A cut that fails after the journal rows were written says the rows were written. - dmarc.Service.Resync and inbound.Service.Resync are serialised like app.Service.Resync (W3's shape, outside its diff). Left as the review's nit: three ApplicationByLogin lookups for a refused MAIL FROM (a refusal is rare; the store is local). go test ./internal/dmarc ./internal/inbound ./internal/store; the import test; GOOS=linux go vet ./test/e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
package e2e
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/smtp"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// limitDomain is a sending domain of its own for the domain-limit case, so
|
|
// that its ceiling of one touches no other step.
|
|
const limitDomain = "limit.e2e.test"
|
|
|
|
// testDomainLimitNotReservedByAStranger is the "done when" of the security
|
|
// audit's M1 (2026-10-17): the level-2 domain limit is the login's domain's.
|
|
// MAIL FROM reaches the journal milter before Postfix holds it to a login, so
|
|
// a client with no login naming a domain there used to take one of that
|
|
// domain's slots and hold it for as long as its transaction stayed open.
|
|
// Here a domain has a limit of one message an hour; a stranger opens the
|
|
// submission port, names the domain in MAIL FROM without signing in and
|
|
// keeps the transaction open, and meanwhile the domain's own application
|
|
// sends its one message - and is refused the second, so the limit is in
|
|
// force. (A RSET between the stranger's MAIL FROMs would end the
|
|
// transaction, which releases the slot under the old code as well: the
|
|
// review of stage 1 found the first version of this step passing on it.)
|
|
func testDomainLimitNotReservedByAStranger(t *testing.T, sc *scenario) {
|
|
domainID, err := sc.panel.addDomain(limitDomain)
|
|
if err != nil {
|
|
t.Fatalf("add %s: %v", limitDomain, err)
|
|
}
|
|
login, password, err := sc.panel.addApplication(domainID, "limit-app", "wildcard", "")
|
|
if err == nil {
|
|
err = waitForSenderMap(login, password, "alerts@"+limitDomain)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("add the application: %v", err)
|
|
}
|
|
resp, body, err := sc.panel.postForm("/outbound/domains/"+domainID+"/settings/ratelimit", url.Values{
|
|
"mode": {"manual"}, "max_messages": {"1"}, "window_seconds": {"3600"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK || resp.Request.URL.Path != "/outbound/domains/"+domainID {
|
|
t.Fatalf("save the domain limit: status %d, landed on %s:\n%.800s", resp.StatusCode, resp.Request.URL.Path, body)
|
|
}
|
|
|
|
stranger, err := strangerHoldsMailFrom("x@" + limitDomain)
|
|
if err != nil {
|
|
t.Fatalf("the stranger's MAIL FROM: %v", err)
|
|
}
|
|
defer stranger()
|
|
|
|
send := func(token string) sendResult {
|
|
return attemptSend(sendAttempt{
|
|
authLogin: login, authPassword: password,
|
|
from: "alerts@" + limitDomain, to: recipient, subject: "e2e " + uniqueToken(token), body: "ok",
|
|
})
|
|
}
|
|
if res := send("m1-own"); !res.ok() {
|
|
t.Fatalf("the domain's own message after a stranger's MAIL FROM: %v", res.firstErr())
|
|
}
|
|
if err := send("m1-second").refusedAt("MAIL", 451, "4.7.1 Service unavailable"); err != nil {
|
|
t.Fatalf("the second message over the domain's limit of one: %v", err)
|
|
}
|
|
}
|
|
|
|
// strangerHoldsMailFrom opens the implicit-TLS submission port, says EHLO
|
|
// and MAIL FROM:<from> without signing in, and leaves the transaction open
|
|
// until the returned func is called. The MAIL FROM is answered 250: the
|
|
// journal milter continues, and Postfix refuses such a client only at RCPT
|
|
// (smtpd_delay_reject).
|
|
func strangerHoldsMailFrom(from string) (func(), error) {
|
|
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 10 * time.Second}, "tcp", smtpsAddr, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
ServerName: selfpostHostname,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c, err := smtp.NewClient(conn, selfpostHostname)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if err := c.Hello("stranger.e2e.test"); err != nil {
|
|
_ = c.Close()
|
|
return nil, err
|
|
}
|
|
if err := c.Mail(from); err != nil {
|
|
_ = c.Close()
|
|
return nil, fmt.Errorf("MAIL FROM: %w", err)
|
|
}
|
|
return func() { _ = c.Quit(); _ = c.Close() }, nil
|
|
}
|