From e853ac35398c432945092f2d20988ac53dbe5910 Mon Sep 17 00:00:00 2001 From: mel Date: Sat, 17 Jan 2026 03:44:01 -0800 Subject: [PATCH] feat(channels): add subscriber fan-out delivery When messages are sent to a channel, subscribers now receive a copy in their inbox with [channel:name] prefix in the subject. Closes: gt-3rldf6 Co-Authored-By: Claude Opus 4.5 --- internal/mail/router.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/mail/router.go b/internal/mail/router.go index 35279206..af29c8a6 100644 --- a/internal/mail/router.go +++ b/internal/mail/router.go @@ -804,6 +804,7 @@ func (r *Router) sendToAnnounce(msg *Message) error { // sendToChannel delivers a message to a beads-native channel. // Creates a message with channel: label for channel queries. +// Also fans out delivery to each subscriber's inbox. // Retention is enforced by the channel's EnforceChannelRetention after message creation. func (r *Router) sendToChannel(msg *Message) error { channelName := parseChannelName(msg.To) @@ -872,7 +873,23 @@ func (r *Router) sendToChannel(msg *Message) error { // Enforce channel retention policy (on-write cleanup) _ = b.EnforceChannelRetention(channelName) - // No notification for channel messages - readers poll or check on their own schedule + // Fan-out delivery: send a copy to each subscriber's inbox + if len(fields.Subscribers) > 0 { + for _, subscriber := range fields.Subscribers { + // Skip self-delivery (don't notify the sender) + if isSelfMail(msg.From, subscriber) { + continue + } + + // Create a copy for this subscriber with channel context in subject + msgCopy := *msg + msgCopy.To = subscriber + msgCopy.Subject = fmt.Sprintf("[channel:%s] %s", channelName, msg.Subject) + + // Best-effort delivery - don't fail the channel send if one subscriber fails + _ = r.sendToSingle(&msgCopy) + } + } return nil }