d55c590c5a
gates / gates (push) Successful in 5m25s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
89 lines
3.7 KiB
Go
89 lines
3.7 KiB
Go
// Package mailhold is the hub's one mail gate: while the marker file `<data_dir>/MAIL-HOLD` exists, the hub sends
|
|
// NO e-mail at all — household or operator, on every path that reaches Resend (the dispatcher's HTTP-API sends, the
|
|
// legacy /notify endpoint, the app-mail SMTP relay).
|
|
//
|
|
// WHY. The 2026-10-09 restore drill (documentation/audits/dooplex-survival-2026-10-09/partD-05-checks.txt) started a
|
|
// hub on a restored database, and within a minute it mailed households the pending "kernel notice" mails.
|
|
// `notifications.operator_enabled: false` did not stop it, because that switch covers the operator channel only. A
|
|
// restored copy is a second hub speaking with the first one's voice; it must start quiet until the operator says so.
|
|
// The marker is created (an empty file is enough) before a restored copy's first start.
|
|
//
|
|
// HELD MAILS ARE DROPPED, NOT QUEUED, and that is deliberate: every mail a restored copy wants to send was decided
|
|
// from a snapshot of the past — a kernel notice for a step the real hub already ran or will run, an alarm about a
|
|
// state that has since changed. Re-sending them after the hold is lifted would deliver stale news as if it were
|
|
// current. After the release, the hub's checkers and boxes produce fresh mails from live state on their own. Each
|
|
// dropped mail is logged once (`[WARN] MAIL-HOLD: not sending <kind> to <who>` — never the address, never the body).
|
|
//
|
|
// The marker is read on every send (one stat of a file in the data dir; no wait on a device the hub does not
|
|
// already depend on for its database). A stat error other than "does not exist" counts as HELD: the gate fails
|
|
// closed, and the operator pages show the banner, so the state is never invisible.
|
|
package mailhold
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// FileName is the marker's name inside the hub's data directory.
|
|
const FileName = "MAIL-HOLD"
|
|
|
|
// ErrHeld is returned by Check for a mail the hold stopped. Callers treat it as "not sent" (never as sent).
|
|
var ErrHeld = errors.New("mail hold: the hub is not sending e-mail (MAIL-HOLD marker present)")
|
|
|
|
// Hold reads and removes the marker. The zero value and a nil *Hold never hold (tests and wiring without a data dir).
|
|
type Hold struct {
|
|
Path string
|
|
Logger *log.Logger
|
|
}
|
|
|
|
// New returns the hold for a data directory.
|
|
func New(dataDir string, logger *log.Logger) *Hold {
|
|
return &Hold{Path: filepath.Join(dataDir, FileName), Logger: logger}
|
|
}
|
|
|
|
// Held reports whether the marker is present (fail-closed on an unreadable state).
|
|
func (h *Hold) Held() bool {
|
|
if h == nil || h.Path == "" {
|
|
return false
|
|
}
|
|
_, err := os.Stat(h.Path)
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false
|
|
}
|
|
h.logf("[WARN] MAIL-HOLD: cannot read the marker state (%v) — treating mail as HELD", err)
|
|
return true
|
|
}
|
|
|
|
// Check is the gate every sender calls immediately before handing a mail to Resend. When held it logs the one WARN
|
|
// line for this mail and returns ErrHeld; otherwise nil. kind names the mail ("customer event backup_failed",
|
|
// "kernel notice", …); who is a customer ID or "operator" — never an address.
|
|
func (h *Hold) Check(kind, who string) error {
|
|
if !h.Held() {
|
|
return nil
|
|
}
|
|
h.logf("[WARN] MAIL-HOLD: not sending %s to %s", kind, who)
|
|
return ErrHeld
|
|
}
|
|
|
|
// Release removes the marker. Removing a marker that is not there is not an error (the release is idempotent).
|
|
func (h *Hold) Release() error {
|
|
if h == nil || h.Path == "" {
|
|
return nil
|
|
}
|
|
if err := os.Remove(h.Path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *Hold) logf(format string, args ...interface{}) {
|
|
if h != nil && h.Logger != nil {
|
|
h.Logger.Printf(format, args...)
|
|
}
|
|
}
|