audit 4: a deleted domain says so, the Outbound log filters by status and day
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
L17: past DeleteDomain both rebuilds run, their errors joined under domain.ErrDeletedIncomplete; the key is removed only when OpenDKIM's tables were rebuilt (they may still name it otherwise); the domain list says the domain is deleted and that Reload configuration finishes it, not "internal error". Tests in domain (the tables failing, the sender map failing) and handlers. The Outbound log's status and day filters (owner, 2026-10-13; the mockup first,80e3979): view.OutLogFilter carried by the poll, the pages, the rows' links and the message's way back; SendLogFilter.Status, Since, Until (whole UTC days, as the Inbound log's); rejected is the three refusals together; a status not in the list is no narrowing. Tests: the filters through the handler, the poll and the way back; the day boundaries in store. The plan's milter-socket step is ticked: done in 2.5.2 (98e09fe). Tested on the test server: go vet + go test of 25 packages, make e2e (all pass, 642 s) on this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/store"
|
||||
)
|
||||
|
||||
// countingApps is fakeApps that counts the sender map's rebuilds and can
|
||||
// fail them.
|
||||
type countingApps struct {
|
||||
fakeApps
|
||||
resyncs int
|
||||
resyncErr error
|
||||
}
|
||||
|
||||
func (c *countingApps) Resync() error {
|
||||
c.resyncs++
|
||||
return c.resyncErr
|
||||
}
|
||||
|
||||
// A deletion that fails after the domain's row is gone says the domain is
|
||||
// deleted, and runs every rebuild anyway: the sender map was left naming the
|
||||
// purged logins when OpenDKIM's tables failed first (audit 2026-10-12, L17).
|
||||
// The key stays while the tables may still name it.
|
||||
func TestDeleteRunsEveryRebuildAndSaysTheDomainIsGone(t *testing.T) {
|
||||
apps := &countingApps{}
|
||||
s, odk := newTestService(t, apps)
|
||||
d, err := s.Add("gone.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyDir := filepath.Join(odk.keysDir, d.Name)
|
||||
apps.resyncs = 0
|
||||
|
||||
odk.reload = func() error { return errors.New("supervisor away") }
|
||||
err = s.Delete(d.ID)
|
||||
if !errors.Is(err, ErrDeletedIncomplete) {
|
||||
t.Fatalf("Delete with OpenDKIM's reload failing = %v, want ErrDeletedIncomplete", err)
|
||||
}
|
||||
if _, err := s.Get(d.ID); !errors.Is(err, store.ErrDomainNotFound) {
|
||||
t.Errorf("the domain is still in the registry: %v", err)
|
||||
}
|
||||
if apps.resyncs != 1 {
|
||||
t.Errorf("the sender map was rebuilt %d times, want once", apps.resyncs)
|
||||
}
|
||||
if _, err := os.Stat(keyDir); err != nil {
|
||||
t.Errorf("the key went while the tables may still name it: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The sender map failing is the same: deleted, said so; the tables were
|
||||
// rebuilt, so the key goes.
|
||||
func TestDeleteWithTheSenderMapFailing(t *testing.T) {
|
||||
apps := &countingApps{resyncErr: errors.New("postfix away")}
|
||||
s, odk := newTestService(t, apps)
|
||||
d, err := s.Add("gone.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyDir := filepath.Join(odk.keysDir, d.Name)
|
||||
if err := s.Delete(d.ID); !errors.Is(err, ErrDeletedIncomplete) {
|
||||
t.Fatalf("Delete with the sender map failing = %v, want ErrDeletedIncomplete", err)
|
||||
}
|
||||
if _, err := os.Stat(keyDir); !os.IsNotExist(err) {
|
||||
t.Errorf("the key is still there after the tables were rebuilt: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/store"
|
||||
@@ -202,28 +203,41 @@ func (s *Service) Delete(id int64) error {
|
||||
if err := s.store.DeleteDomain(id); err != nil {
|
||||
return err
|
||||
}
|
||||
// From here the domain is deleted, whatever fails after: each rebuild
|
||||
// runs - the sender map was left with the purged logins when OpenDKIM's
|
||||
// failed first - and a failure says the domain is gone (audit
|
||||
// 2026-10-12, L17).
|
||||
var errs []error
|
||||
tablesRebuilt := true
|
||||
if err := s.resync(); err != nil {
|
||||
return err
|
||||
tablesRebuilt = false
|
||||
errs = append(errs, fmt.Errorf("rebuild OpenDKIM's tables: %w", err))
|
||||
}
|
||||
if err := s.apps.Resync(); err != nil {
|
||||
return fmt.Errorf("rebuild sender map after deleting %s: %w", d.Name, err)
|
||||
errs = append(errs, fmt.Errorf("rebuild the sender map: %w", err))
|
||||
}
|
||||
// A sending domain named like the server shared its key with the server's
|
||||
// own mail. The resync above kept the name in the tables under the default
|
||||
// selector: the same key if that was the domain's selector, a new one -
|
||||
// with a new record to publish, which Settings shows - if it was not.
|
||||
// Either way the directory holds the key now in use.
|
||||
if d.Name == s.serverName {
|
||||
return nil
|
||||
// Either way the directory holds the key now in use. And while the tables
|
||||
// may still name the deleted domain's key, the key stays.
|
||||
if d.Name != s.serverName && tablesRebuilt {
|
||||
if err := s.odk.RemoveKey(d.Name); err != nil {
|
||||
errs = append(errs, fmt.Errorf("remove its DKIM key: %w", err))
|
||||
}
|
||||
}
|
||||
if err := s.odk.RemoveKey(d.Name); err != nil {
|
||||
// The domain is gone from the registry and tables; a leftover key
|
||||
// directory is harmless. Surface it so it is not silently ignored.
|
||||
return fmt.Errorf("domain deleted but key cleanup failed: %w", err)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%w (%s): %w", ErrDeletedIncomplete, d.Name, errors.Join(errs...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrDeletedIncomplete is a domain deleted from the registry, with a step
|
||||
// after it that failed: the mail configuration still names it until a
|
||||
// rebuild succeeds (Reload configuration).
|
||||
var ErrDeletedIncomplete = errors.New("domain deleted, but the mail configuration was not fully rewritten after it")
|
||||
|
||||
// DKIMRecord returns the DNS TXT record to publish for a domain (product.md).
|
||||
func (s *Service) DKIMRecord(d store.Domain) (DKIMRecord, error) {
|
||||
return s.odk.Record(d.Name, d.DKIMSelector)
|
||||
|
||||
Reference in New Issue
Block a user