mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-10-29 13:42:30 +00:00
feat(luahost): Lua-controlled settings and decoded header access (#630)
* feat(luahost): capture config settings set by Lua scripts Add inbucket.config, a Lua-settable settings namespace, with its first field decode_headers registering additional message headers for Inbucket to decode. Values are captured once at startup from the first pooled LState and exposed to the assembly via Host.ScriptConfig(). Settings that only have value to the Lua script belong here rather than in env config. No consumers of decode_headers yet; that follows. * feat(message): expose decoded headers to extensions StoreManager gains ExtraDecodeHeaders, passed through to enmime.DecodeHeaders, and the decoded header set (defaults plus any registered extras) is attached to the InboundMessage event as Headers (net/textproto.MIMEHeader). Lua scripts access it via the read-only inbound_message.header field, added with the new inbound_header binding. Lookups are exact-then- case-insensitive, since enmime canonicalizes keys with email-specific rules that differ from net/textproto. FullAssembly wires ScriptConfig().DecodeHeaders into StoreManager. Part of #627. * docs(extension): document shared and untrusted header semantics Reviewer note: BeforeMessageStored listeners receive the same decoded header map, so a Go listener's mutations are visible to later listeners. Also record that sender-supplied header values (Authentication-Results, X-Spam-Status) are untrusted unless the deployment controls provenance. Part of #627. --------- Co-authored-by: hillyerd-bot <hillyerd-bot@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -41,6 +42,9 @@ type StoreManager struct {
|
||||
AddrPolicy *policy.Addressing
|
||||
Store storage.Store
|
||||
ExtHost *extension.Host
|
||||
// ExtraDecodeHeaders names message headers beyond the default set to
|
||||
// decode and expose to extensions via event.InboundMessage.Headers.
|
||||
ExtraDecodeHeaders []string
|
||||
}
|
||||
|
||||
// Deliver submits a new message to the store.
|
||||
@@ -53,7 +57,7 @@ func (s *StoreManager) Deliver(
|
||||
logger := log.With().Str("module", "message").Logger()
|
||||
|
||||
// Parse envelope headers.
|
||||
header, err := enmime.DecodeHeaders(source)
|
||||
header, err := enmime.DecodeHeaders(source, s.ExtraDecodeHeaders...)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to parse message headers")
|
||||
return err
|
||||
@@ -92,6 +96,7 @@ func (s *StoreManager) Deliver(
|
||||
To: toAddrs,
|
||||
Subject: subject,
|
||||
Size: int64(len(source)),
|
||||
Headers: textproto.MIMEHeader(header),
|
||||
}
|
||||
|
||||
extResult := s.ExtHost.Events.BeforeMessageStored.Emit(inbound)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -106,6 +107,64 @@ func TestDeliverRespectsRecipientPolicy(t *testing.T) {
|
||||
assertMessageCount(t, sm, "u2@example.com", 1)
|
||||
}
|
||||
|
||||
// Delivers a message with an X-Spam-Status header, capturing the
|
||||
// InboundMessage event seen by a listener.
|
||||
func deliverWithSpamHeader(t *testing.T, sm *message.StoreManager, extHost *extension.Host) event.InboundMessage {
|
||||
t.Helper()
|
||||
|
||||
var got event.InboundMessage
|
||||
extHost.Events.BeforeMessageStored.AddListener(
|
||||
"test",
|
||||
func(msg event.InboundMessage) *event.InboundMessage {
|
||||
got = msg
|
||||
return nil
|
||||
})
|
||||
|
||||
origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com")
|
||||
recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com")
|
||||
if err := sm.Deliver(
|
||||
origin,
|
||||
[]*policy.Recipient{recip1},
|
||||
"Received: xyz\r\n",
|
||||
[]byte("From: from@example.com\nSubject: tsub\nX-Spam-Status: Yes\nX-Not-Registered: no\n\ntest email"),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return got
|
||||
}
|
||||
|
||||
func TestDeliverDecodesExtraHeaders(t *testing.T) {
|
||||
sm, extHost := testStoreManager()
|
||||
sm.ExtraDecodeHeaders = []string{"x-spam-status"} // Not canonical on purpose.
|
||||
|
||||
got := deliverWithSpamHeader(t, sm, extHost)
|
||||
|
||||
// Registered extra header is decoded, with a canonical key.
|
||||
assert.Equal(t, "Yes", got.Headers.Get("X-Spam-Status"))
|
||||
|
||||
// Default set is still decoded.
|
||||
assert.Equal(t, "tsub", got.Headers.Get("Subject"))
|
||||
assert.Equal(t, "from@example.com", got.Headers.Get("From"))
|
||||
|
||||
// Unregistered headers are not decoded.
|
||||
_, ok := got.Headers[textproto.CanonicalMIMEHeaderKey("X-Not-Registered")]
|
||||
assert.False(t, ok, "unregistered header should not be decoded")
|
||||
}
|
||||
|
||||
func TestDeliverDefaultHeaderSetOnly(t *testing.T) {
|
||||
sm, extHost := testStoreManager()
|
||||
|
||||
got := deliverWithSpamHeader(t, sm, extHost)
|
||||
|
||||
// Without registration, the extra header stays undecoded.
|
||||
_, ok := got.Headers[textproto.CanonicalMIMEHeaderKey("X-Spam-Status")]
|
||||
assert.False(t, ok, "unregistered header should not be decoded")
|
||||
|
||||
// Defaults are present.
|
||||
assert.Equal(t, "tsub", got.Headers.Get("Subject"))
|
||||
}
|
||||
|
||||
func TestDeliverEmitsBeforeMessageStoredEventToHeader(t *testing.T) {
|
||||
sm, extHost := testStoreManager()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user