panel-notifications: guide, architecture, security; refusals looked for every minute
guide.md gains Notifications: the four things, when each is said and said again, who is told, the two switches, and the two DNS records of the server's own name that make the mail arrive. architecture.md: the notifier as the fifth role, its readings, state, recipients and sending; the server's own key in OpenDKIM's tables; non_smtpd_milters. security.md: the panel submits mail of its own - how, to whom, and what text from outside can reach a body. That text - a queued message's sender, a DNS finding - is reduced to printable ASCII and cut before it is printed. Refused mail is looked for every minute instead of every five: it is one indexed query, and a refusal is mail not being sent now. The image's note on the panel's postdrop membership no longer calls it read-side only. go test ./internal/notify passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,8 +29,9 @@ const (
|
||||
// at. A var-free constant: the rules below count in checks.
|
||||
CheckInterval = time.Hour
|
||||
// LimitInterval is how often refused mail is looked for. A refusal is
|
||||
// reported at once rather than on the hour: it is mail not being sent now.
|
||||
LimitInterval = 5 * time.Minute
|
||||
// reported within the minute rather than on the hour: it is mail not being
|
||||
// sent now. The look is one indexed query.
|
||||
LimitInterval = time.Minute
|
||||
|
||||
// dnsChecks is how many checks in a row must agree before a DNS mismatch is
|
||||
// reported, and before it is reported gone. One check is a resolver's bad
|
||||
|
||||
+20
-2
@@ -35,6 +35,24 @@ func compose(hostname, subject string, paragraphs ...string) message {
|
||||
|
||||
func stamp(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 UTC") }
|
||||
|
||||
// plain makes text that came from outside fit for a message body: a queued
|
||||
// message's sender is whatever its sender wrote, and a DNS finding may quote a
|
||||
// name. What is not printable ASCII becomes "?", and it is cut to max bytes -
|
||||
// a notification is read by a person, and nothing in it is theirs to act on
|
||||
// but the panel's own words.
|
||||
func plain(s string, max int) string {
|
||||
if len(s) > max {
|
||||
s = s[:max]
|
||||
}
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
if c < 0x20 || c > 0x7e {
|
||||
b[i] = '?'
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// days words a duration in whole days, the unit these messages count in.
|
||||
func days(d time.Duration) string {
|
||||
n := int(d.Hours() / 24)
|
||||
@@ -49,7 +67,7 @@ func dnsProblem(hostname, domain string, faults []dnsFault, since time.Time) mes
|
||||
lines := make([]string, len(faults))
|
||||
for i, f := range faults {
|
||||
names[i] = f.Record
|
||||
lines[i] = f.Record + ": " + f.Detail
|
||||
lines[i] = f.Record + ": " + plain(f.Detail, 600)
|
||||
}
|
||||
return compose(hostname,
|
||||
fmt.Sprintf("%s: %s no longer matches", domain, strings.Join(names, ", ")),
|
||||
@@ -91,7 +109,7 @@ func queueStuck(hostname string, old []postfix.QueuedMessage, now time.Time) mes
|
||||
if sender == "" {
|
||||
sender = "(bounce)"
|
||||
}
|
||||
fmt.Fprintf(&b, "%s from %s waiting %s, since %s\n", m.ID, sender, days(now.Sub(m.Arrival)), stamp(m.Arrival))
|
||||
fmt.Fprintf(&b, "%s from %s waiting %s, since %s\n", plain(m.ID, 24), plain(sender, 120), days(now.Sub(m.Arrival)), stamp(m.Arrival))
|
||||
}
|
||||
what := "A message has"
|
||||
if len(old) > 1 {
|
||||
|
||||
Reference in New Issue
Block a user