mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-10-29 05:32:31 +00:00
bd4cc4b842
* 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>
235 lines
5.4 KiB
Go
235 lines
5.4 KiB
Go
package luahost
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
const (
|
|
inbucketName = "inbucket"
|
|
inbucketBeforeName = "inbucket_before"
|
|
inbucketAfterName = "inbucket_after"
|
|
)
|
|
|
|
// Inbucket is the primary Lua interface data structure.
|
|
type Inbucket struct {
|
|
Config InbucketConfig
|
|
After InbucketAfterFuncs
|
|
Before InbucketBeforeFuncs
|
|
}
|
|
|
|
// InbucketAfterFuncs holds references to Lua extension functions to be called async
|
|
// after Inbucket handles an event.
|
|
type InbucketAfterFuncs struct {
|
|
MessageDeleted *lua.LFunction
|
|
MessageStored *lua.LFunction
|
|
}
|
|
|
|
// InbucketBeforeFuncs holds references to Lua extension functions to be called
|
|
// before Inbucket handles an event.
|
|
type InbucketBeforeFuncs struct {
|
|
MailFromAccepted *lua.LFunction
|
|
MessageStored *lua.LFunction
|
|
RcptToAccepted *lua.LFunction
|
|
}
|
|
|
|
func registerInbucketTypes(ls *lua.LState) {
|
|
// inbucket type.
|
|
mt := ls.NewTypeMetatable(inbucketName)
|
|
ls.SetField(mt, "__index", ls.NewFunction(inbucketIndex))
|
|
|
|
// inbucket global var.
|
|
ud := wrapInbucket(ls, &Inbucket{})
|
|
ls.SetGlobal(inbucketName, ud)
|
|
|
|
// inbucket.config type.
|
|
registerInbucketConfigType(ls)
|
|
|
|
// inbucket.after type.
|
|
mt = ls.NewTypeMetatable(inbucketAfterName)
|
|
ls.SetField(mt, "__index", ls.NewFunction(inbucketAfterIndex))
|
|
ls.SetField(mt, "__newindex", ls.NewFunction(inbucketAfterNewIndex))
|
|
|
|
// inbucket.before type.
|
|
mt = ls.NewTypeMetatable(inbucketBeforeName)
|
|
ls.SetField(mt, "__index", ls.NewFunction(inbucketBeforeIndex))
|
|
ls.SetField(mt, "__newindex", ls.NewFunction(inbucketBeforeNewIndex))
|
|
}
|
|
|
|
func wrapInbucket(ls *lua.LState, val *Inbucket) *lua.LUserData {
|
|
ud := ls.NewUserData()
|
|
ud.Value = val
|
|
ls.SetMetatable(ud, ls.GetTypeMetatable(inbucketName))
|
|
|
|
return ud
|
|
}
|
|
|
|
func wrapInbucketAfter(ls *lua.LState, val *InbucketAfterFuncs) *lua.LUserData {
|
|
ud := ls.NewUserData()
|
|
ud.Value = val
|
|
ls.SetMetatable(ud, ls.GetTypeMetatable(inbucketAfterName))
|
|
|
|
return ud
|
|
}
|
|
|
|
func wrapInbucketBefore(ls *lua.LState, val *InbucketBeforeFuncs) *lua.LUserData {
|
|
ud := ls.NewUserData()
|
|
ud.Value = val
|
|
ls.SetMetatable(ud, ls.GetTypeMetatable(inbucketBeforeName))
|
|
|
|
return ud
|
|
}
|
|
|
|
func getInbucket(ls *lua.LState) (*Inbucket, error) {
|
|
lv := ls.GetGlobal(inbucketName)
|
|
if lv == nil {
|
|
return nil, errors.New("inbucket object was nil")
|
|
}
|
|
|
|
ud, ok := lv.(*lua.LUserData)
|
|
if !ok {
|
|
return nil, fmt.Errorf("inbucket object was type %s instead of UserData", lv.Type())
|
|
}
|
|
|
|
val, ok := ud.Value.(*Inbucket)
|
|
if !ok {
|
|
return nil, fmt.Errorf("inbucket object (%v) could not be cast", ud.Value)
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
func checkInbucket(ls *lua.LState, pos int) *Inbucket {
|
|
ud := ls.CheckUserData(pos)
|
|
if val, ok := ud.Value.(*Inbucket); ok {
|
|
return val
|
|
}
|
|
ls.ArgError(1, inbucketName+" expected")
|
|
return nil
|
|
}
|
|
|
|
func checkInbucketAfter(ls *lua.LState, pos int) *InbucketAfterFuncs {
|
|
ud := ls.CheckUserData(pos)
|
|
if val, ok := ud.Value.(*InbucketAfterFuncs); ok {
|
|
return val
|
|
}
|
|
ls.ArgError(1, inbucketAfterName+" expected")
|
|
return nil
|
|
}
|
|
|
|
func checkInbucketBefore(ls *lua.LState, pos int) *InbucketBeforeFuncs {
|
|
ud := ls.CheckUserData(pos)
|
|
if val, ok := ud.Value.(*InbucketBeforeFuncs); ok {
|
|
return val
|
|
}
|
|
ls.ArgError(1, inbucketBeforeName+" expected")
|
|
return nil
|
|
}
|
|
|
|
// inbucket getter.
|
|
func inbucketIndex(ls *lua.LState) int {
|
|
ib := checkInbucket(ls, 1)
|
|
field := ls.CheckString(2)
|
|
|
|
// Push the requested field's value onto the stack.
|
|
switch field {
|
|
case "config":
|
|
ls.Push(wrapInbucketConfig(ls, &ib.Config))
|
|
case "after":
|
|
ls.Push(wrapInbucketAfter(ls, &ib.After))
|
|
case "before":
|
|
ls.Push(wrapInbucketBefore(ls, &ib.Before))
|
|
default:
|
|
// Unknown field.
|
|
ls.Push(lua.LNil)
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
// inbucket.after getter.
|
|
func inbucketAfterIndex(ls *lua.LState) int {
|
|
after := checkInbucketAfter(ls, 1)
|
|
field := ls.CheckString(2)
|
|
|
|
// Push the requested field's value onto the stack.
|
|
switch field {
|
|
case "message_deleted":
|
|
ls.Push(funcOrNil(after.MessageDeleted))
|
|
case "message_stored":
|
|
ls.Push(funcOrNil(after.MessageStored))
|
|
default:
|
|
// Unknown field.
|
|
ls.Push(lua.LNil)
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
// inbucket.after setter.
|
|
func inbucketAfterNewIndex(ls *lua.LState) int {
|
|
m := checkInbucketAfter(ls, 1)
|
|
index := ls.CheckString(2)
|
|
|
|
switch index {
|
|
case "message_deleted":
|
|
m.MessageDeleted = ls.CheckFunction(3)
|
|
case "message_stored":
|
|
m.MessageStored = ls.CheckFunction(3)
|
|
default:
|
|
ls.RaiseError("invalid inbucket.after index %q", index)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// inbucket.before getter.
|
|
func inbucketBeforeIndex(ls *lua.LState) int {
|
|
before := checkInbucketBefore(ls, 1)
|
|
field := ls.CheckString(2)
|
|
|
|
// Push the requested field's value onto the stack.
|
|
switch field {
|
|
case "mail_from_accepted":
|
|
ls.Push(funcOrNil(before.MailFromAccepted))
|
|
case "message_stored":
|
|
ls.Push(funcOrNil(before.MessageStored))
|
|
case "rcpt_to_accepted":
|
|
ls.Push(funcOrNil(before.RcptToAccepted))
|
|
default:
|
|
// Unknown field.
|
|
ls.Push(lua.LNil)
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
// inbucket.before setter.
|
|
func inbucketBeforeNewIndex(ls *lua.LState) int {
|
|
m := checkInbucketBefore(ls, 1)
|
|
index := ls.CheckString(2)
|
|
|
|
switch index {
|
|
case "mail_from_accepted":
|
|
m.MailFromAccepted = ls.CheckFunction(3)
|
|
case "message_stored":
|
|
m.MessageStored = ls.CheckFunction(3)
|
|
case "rcpt_to_accepted":
|
|
m.RcptToAccepted = ls.CheckFunction(3)
|
|
default:
|
|
ls.RaiseError("invalid inbucket.before index %q", index)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func funcOrNil(f *lua.LFunction) lua.LValue {
|
|
if f == nil {
|
|
return lua.LNil
|
|
}
|
|
|
|
return f
|
|
}
|