release: 1.4.0: Optional inbound relay (backup-MX) on port 25

Optional inbound relay (backup-MX / forwarder) on port 25, off by default. Mail is accepted only for domains configured in the panel and forwarded to an upstream; there are still no mailboxes. A dedicated security review of the inbound path is still open on the plan (Fable).
Added:
  - `INBOUND_RELAY_ENABLE` (default `false`): when true, Postfix listens on
  - Panel **Inbound** pages (global administrator): domains, upstream host/port
  - Optional `INBOUND_ANTISPAM_MILTER` on the inbound listener only, plus
  - Inbound rate limit (`INBOUND_RATE_LIMIT_MESSAGES_PER_IP`, default 20) and
Changed:
  - Compose publishes host port 25 even when the flag is off (nothing listens
  - Full-backup restore Resync also rebuilds inbound maps when the flag is on.

Full detail: CHANGELOG.md (1.4.0).
This commit is contained in:
mix
2026-09-22 15:24:52 +03:00
parent 0730b2090e
commit 656c4d4d7f
50 changed files with 2512 additions and 125 deletions
+41 -6
View File
@@ -178,6 +178,35 @@ func TestNavLeadsWithStatusAndPointsDomainsAtItsOwnPath(t *testing.T) {
if strings.Index(out, "Status") > strings.Index(out, "Domains") {
t.Errorf("Status is not the first navigation entry:\n%s", out)
}
if strings.Contains(out, `href="/inbound"`) || strings.Contains(out, "Inbound") {
t.Errorf("Inbound nav is shown while InboundEnabled is unset:\n%s", out)
}
}
func TestNavShowsInboundWhenEnabled(t *testing.T) {
engine, err := New("test")
if err != nil {
t.Fatalf("New: %v", err)
}
engine.SetInboundEnabled(true)
var buf bytes.Buffer
if err := engine.Page("status").ExecuteTemplate(&buf, "nav", map[string]any{
"User": "admin",
"Active": "status",
"IsGlobal": true,
"InboundEnabled": true,
}); err != nil {
t.Fatalf("execute nav: %v", err)
}
out := buf.String()
if !strings.Contains(out, `href="/inbound"`) || !strings.Contains(out, "Inbound") {
t.Errorf("Inbound nav is missing while InboundEnabled is true:\n%s", out)
}
dom := strings.Index(out, `href="/domains"`)
inb := strings.Index(out, `href="/inbound"`)
if dom < 0 || inb < 0 || inb < dom {
t.Errorf("Inbound should follow Domains:\n%s", out)
}
}
// Whether a page takes the whole column or the reading measure is declared by
@@ -190,7 +219,11 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
if err != nil {
t.Fatalf("New: %v", err)
}
wide := map[string]bool{"settings": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true, "domain_detail": true}
wide := map[string]bool{
"settings": true, "deliveries": true, "delivery": true, "mail_queue": true,
"status": true, "system_log": true, "domain_detail": true,
"inbound": true, "inbound_domain": true,
}
for name, page := range engine.Pages() {
var buf bytes.Buffer
if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil {
@@ -281,11 +314,13 @@ func TestSettingsPageDocumentsRateLimits(t *testing.T) {
func TestDrillDownPagesPlaceBackLinkAboveContent(t *testing.T) {
drillDown := map[string]bool{
"user_form.html": true,
"user_delete.html": true,
"domain_detail.html": true,
"domain_delete.html": true,
"delivery.html": true,
"user_form.html": true,
"user_delete.html": true,
"domain_detail.html": true,
"domain_delete.html": true,
"inbound_domain.html": true,
"inbound_delete.html": true,
"delivery.html": true,
}
forEachTemplate(t, func(name, body string) {
if !drillDown[name] {