Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,11 +613,15 @@ For a reply (a draft with `pid`), the route first verifies that every remote
recipient domain can actually accept it: per the fmsg spec a host rejects a
reply whose parent it has not stored (response code 6), so if the parent was
never addressed to a recipient's domain — or every delivery attempt of the
parent to that domain concluded in rejection — the send is refused with `409`
naming the domain(s) and the remedy (add the recipients to the parent via
add-to, or start a new thread). Domains where the parent's delivery is still
in flight are allowed; the reply's parent's own originating domain always
passes (it retains its outgoing messages). Local recipients are unaffected.
parent to that domain by this host concluded in rejection — the send is
refused with `409` naming the domain(s) and the remedy (add the recipients to
the parent via add-to, or start a new thread). Domains where the parent's
delivery is still in flight are allowed, as are domains whose delivery was
another host's responsibility (a received parent): this host cannot know a
third-party delivery's outcome, so the reply is attempted and the wire's
"parent not found" rejection remains the arbiter. The reply's parent's own
originating domain always passes (it retains its outgoing messages). Local
recipients are unaffected.

**Response:** `200 OK` with `{"id": <int>, "time": <float64>}`.

Expand Down
22 changes: 20 additions & 2 deletions internal/handlers/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,19 @@ func (h *MessageHandler) resolveLocalDelivery(ctx context.Context, table string,

// parentDomainDelivery summarizes the parent message's recorded delivery for
// one recipient domain.
// Local sentinels fmsgd records in response_code (see fmsgd dd.sql):
// -1 marks a delivery attempt that got no response (retryable), -2 marks a
// recipient recorded from a received exchange — that delivery was another
// host's responsibility and this host has no knowledge of its outcome.
const (
localResponseCodeNoResponse = -1
localResponseCodeNotOurDelivery = -2
)

type parentDomainDelivery struct {
delivered bool // at least one recipient there was delivered (parent is stored)
pending bool // at least one recipient there has no outcome recorded yet
otherHost bool // delivery there was another host's responsibility (parent was received)
codes []int // failure response codes recorded for the domain
}

Expand All @@ -164,8 +174,12 @@ func undeliverableReplyDomains(replyDomains []string, parentFromDomain string, b
switch {
case !ok:
blocked = append(blocked, fmt.Sprintf("%s: the message being replied to was never addressed to this domain", d))
case s.delivered || s.pending:
// stored there, or still in flight
case s.delivered || s.pending || s.otherHost:
// Stored there, still in flight, or another host's delivery
// (a received parent): this host cannot know a third-party
// delivery's outcome, so the reply is attempted — the wire's
// "parent not found" rejection (code 6) remains the arbiter
// in the rare case the originating host in fact failed.
default:
blocked = append(blocked, fmt.Sprintf("%s: delivery of the message being replied to failed there (response code(s) %v)", d, s.codes))
}
Expand Down Expand Up @@ -229,6 +243,10 @@ func (h *MessageHandler) parentDeliveryByDomain(ctx context.Context, parentID in
switch {
case delivered:
s.delivered = true
case code != nil && *code == localResponseCodeNotOurDelivery:
s.otherHost = true
case code != nil && *code == localResponseCodeNoResponse:
s.pending = true // retryable: the sender will attempt again
case code != nil:
s.codes = append(s.codes, *code)
default:
Expand Down
4 changes: 4 additions & 0 deletions internal/handlers/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ func TestUndeliverableReplyDomains(t *testing.T) {
"failed.example": {codes: []int{6}},
"mixed.example": {delivered: true, codes: []int{100}},
"partfail.example": {pending: true, codes: []int{6}},
"otherhost.example": {otherHost: true},
"otherfail.example": {otherHost: true, codes: []int{4}},
}
cases := []struct {
name string
Expand All @@ -302,6 +304,8 @@ func TestUndeliverableReplyDomains(t *testing.T) {
{"in-flight parent passes", []string{"pending.example"}, "origin.example", 0},
{"partially failed but still pending passes", []string{"partfail.example"}, "origin.example", 0},
{"delivered outweighs a failed sibling", []string{"mixed.example"}, "origin.example", 0},
{"another host's delivery passes", []string{"otherhost.example"}, "origin.example", 0},
{"another host's delivery outweighs a failed sibling", []string{"otherfail.example"}, "origin.example", 0},
{"originating domain always passes", []string{"origin.example"}, "origin.example", 0},
{"originating domain passes case-insensitively", []string{"Origin.Example"}, "origin.example", 0},
{"never-addressed domain blocked", []string{"stranger.example"}, "origin.example", 1},
Expand Down
Loading