hub v0.143.1 (code): Reply-To on household mails (decision 174); R7 told-kernel refusal is temporary (R-898 test)
gates / gates (push) Successful in 3m21s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-07 18:47:41 +02:00
parent a0ff737ebb
commit 87765bfa63
6 changed files with 130 additions and 0 deletions
+15
View File
@@ -895,6 +895,9 @@ func (d *Dispatcher) sendEmail(to, subject, textBody string, headers map[string]
if len(headers) > 0 {
payload["headers"] = headers
}
if rt := d.replyToFor(to); rt != "" {
payload["reply_to"] = rt
}
jsonData, err := json.Marshal(payload)
if err != nil {
@@ -922,6 +925,18 @@ func (d *Dispatcher) sendEmail(to, subject, textBody string, headers map[string]
return nil
}
// replyToFor is the Reply-To of a mail (`09` §3 decision 174): every HOUSEHOLD mail invites a reply or contact — the
// kernel notice ("reply to this e-mail"), the event sign-off ("contact your operator"), the setup and link mails — and
// it is sent from monitoring@, which no person reads. A reply goes to the operator's address. A mail TO the operator
// gets none (a reply to it is the operator's own). "" when no operator address is configured. Pinned by TestReplyTo_*.
func (d *Dispatcher) replyToFor(to string) string {
op := strings.TrimSpace(d.operatorEmail)
if op == "" || strings.EqualFold(strings.TrimSpace(to), op) {
return ""
}
return op
}
func isEventEnabled(enabledEvents []string, eventType string) bool {
for _, e := range enabledEvents {
if e == eventType {
+45
View File
@@ -0,0 +1,45 @@
package notify
import (
"encoding/json"
"io"
"log"
"net/http"
"os"
"testing"
)
type captureRT struct{ bodies []map[string]any }
func (c *captureRT) RoundTrip(r *http.Request) (*http.Response, error) {
b, _ := io.ReadAll(r.Body)
var m map[string]any
_ = json.Unmarshal(b, &m)
c.bodies = append(c.bodies, m)
return &http.Response{StatusCode: 200, Body: io.NopCloser(nil), Header: http.Header{}}, nil
}
// `09` §3 decision 174: a household mail carries Reply-To = the operator's address (it is sent from monitoring@, and the
// kernel notice says "reply to this e-mail"); a mail to the operator carries none.
// COMPANION RED-PROOF (observed): drop the reply_to line in sendEmail → "household mail has no reply_to".
func TestReplyTo_HouseholdMailRepliesReachTheOperator(t *testing.T) {
d := NewDispatcher(nil, "k", "monitoring@felhom.eu", "admin@felhom.eu", true, log.New(os.Stderr, "", 0))
c := &captureRT{}
d.httpClient = &http.Client{Transport: c}
if err := d.sendEmail("household@example.com", "s", "b", nil); err != nil {
t.Fatal(err)
}
if err := d.sendEmail("admin@felhom.eu", "s", "b", nil); err != nil {
t.Fatal(err)
}
if got := c.bodies[0]["reply_to"]; got != "admin@felhom.eu" {
t.Fatalf("household mail has no reply_to to the operator: %v", c.bodies[0])
}
if _, ok := c.bodies[1]["reply_to"]; ok {
t.Fatalf("a mail to the operator must carry no reply_to: %v", c.bodies[1])
}
none := NewDispatcher(nil, "k", "monitoring@felhom.eu", "", true, log.New(os.Stderr, "", 0))
if none.replyToFor("x@y") != "" {
t.Fatal("no operator address → no reply_to")
}
}