mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 09:37:02 +00:00
feat: Add SMTPResponse type for extensions (#539)
Signed-off-by: James Hillyerd <james@hillyerd.com>
This commit is contained in:
9
.luarc.json
Normal file
9
.luarc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"runtime.version": "Lua 5.1",
|
||||
"diagnostics": {
|
||||
"globals": [
|
||||
"inbucket",
|
||||
"smtp"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// ActionDefer defers decision to built-in Inbucket logic.
|
||||
ActionDefer = iota
|
||||
// ActionAllow explicitly allows this event.
|
||||
ActionAllow
|
||||
// ActionDeny explicitly deny this event, typically with specified SMTP error.
|
||||
ActionDeny
|
||||
)
|
||||
|
||||
// AddressParts contains the local and domain parts of an email address.
|
||||
type AddressParts struct {
|
||||
Local string
|
||||
@@ -31,3 +40,10 @@ type MessageMetadata struct {
|
||||
Size int64
|
||||
Seen bool
|
||||
}
|
||||
|
||||
// SMTPResponse describes the response to an SMTP policy check.
|
||||
type SMTPResponse struct {
|
||||
Action int // ActionDefer, ActionAllow, etc.
|
||||
ErrorCode int // SMTP error code to respond with on deny.
|
||||
ErrorMsg string // SMTP error message to respond with on deny.
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type Host struct {
|
||||
type Events struct {
|
||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||
BeforeMailAccepted EventBroker[event.AddressParts, bool]
|
||||
BeforeMailAccepted EventBroker[event.AddressParts, event.SMTPResponse]
|
||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const policyName = "policy"
|
||||
|
||||
func registerPolicyType(ls *lua.LState) {
|
||||
mt := ls.NewTypeMetatable(policyName)
|
||||
ls.SetGlobal(policyName, mt)
|
||||
|
||||
// Static attributes.
|
||||
ls.SetField(mt, "allow", lua.LTrue)
|
||||
ls.SetField(mt, "deny", lua.LFalse)
|
||||
ls.SetField(mt, "defer", lua.LNil)
|
||||
}
|
||||
54
pkg/extension/luahost/bind_smtpresponse.go
Normal file
54
pkg/extension/luahost/bind_smtpresponse.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const smtpResponseName = "smtp"
|
||||
|
||||
func registerSMTPResponseType(ls *lua.LState) {
|
||||
mt := ls.NewTypeMetatable(smtpResponseName)
|
||||
ls.SetGlobal(smtpResponseName, mt)
|
||||
|
||||
// Static attributes.
|
||||
ls.SetField(mt, "allow", ls.NewFunction(newSMTPResponse(event.ActionAllow)))
|
||||
ls.SetField(mt, "defer", ls.NewFunction(newSMTPResponse(event.ActionDefer)))
|
||||
ls.SetField(mt, "deny", ls.NewFunction(newSMTPResponse(event.ActionDeny)))
|
||||
}
|
||||
|
||||
func newSMTPResponse(action int) func(*lua.LState) int {
|
||||
return func(ls *lua.LState) int {
|
||||
val := &event.SMTPResponse{Action: action}
|
||||
|
||||
if action == event.ActionDeny {
|
||||
// Optionally accept error code and message.
|
||||
val.ErrorCode = ls.OptInt(1, 550)
|
||||
val.ErrorMsg = ls.OptString(2, "Mail denied by policy")
|
||||
}
|
||||
|
||||
ud := wrapSMTPResponse(ls, val)
|
||||
ls.Push(ud)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func wrapSMTPResponse(ls *lua.LState, val *event.SMTPResponse) *lua.LUserData {
|
||||
ud := ls.NewUserData()
|
||||
ud.Value = val
|
||||
ls.SetMetatable(ud, ls.GetTypeMetatable(smtpResponseName))
|
||||
|
||||
return ud
|
||||
}
|
||||
|
||||
func unwrapSMTPResponse(lv lua.LValue) (*event.SMTPResponse, error) {
|
||||
if ud, ok := lv.(*lua.LUserData); ok {
|
||||
if v, ok := ud.Value.(*event.SMTPResponse); ok {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("expected SMTPResponse, got %q", lv.Type().String())
|
||||
}
|
||||
40
pkg/extension/luahost/bind_smtpresponse_test.go
Normal file
40
pkg/extension/luahost/bind_smtpresponse_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSMTPResponseConstructors(t *testing.T) {
|
||||
check := func(script string, want event.SMTPResponse) {
|
||||
t.Helper()
|
||||
ls, _ := test.NewLuaState()
|
||||
registerSMTPResponseType(ls)
|
||||
require.NoError(t, ls.DoString(script))
|
||||
|
||||
got, err := unwrapSMTPResponse(ls.Get(-1))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &want, got)
|
||||
}
|
||||
|
||||
check("return smtp.defer()", event.SMTPResponse{Action: event.ActionDefer})
|
||||
check("return smtp.allow()", event.SMTPResponse{Action: event.ActionAllow})
|
||||
|
||||
// Verify deny() has default code & msg.
|
||||
check("return smtp.deny()", event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 550,
|
||||
ErrorMsg: "Mail denied by policy",
|
||||
})
|
||||
|
||||
// Verify defaults can be overridden.
|
||||
check("return smtp.deny(123, 'bacon')", event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 123,
|
||||
ErrorMsg: "bacon",
|
||||
})
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func (h *Host) handleAfterMessageStored(msg event.MessageMetadata) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *bool {
|
||||
func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *event.SMTPResponse {
|
||||
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_accepted")
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -169,16 +169,16 @@ func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *bool {
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
if lval.Type() == lua.LTNil {
|
||||
if lval.Type() == lua.LTNil || lua.LVIsFalse(lval) {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := true
|
||||
if lua.LVIsFalse(lval) {
|
||||
result = false
|
||||
result, err := unwrapSMTPResponse(lval)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||
}
|
||||
|
||||
return &result
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.InboundMessage {
|
||||
|
||||
@@ -109,29 +109,36 @@ func TestBeforeMailAccepted(t *testing.T) {
|
||||
// Register lua event listener.
|
||||
script := `
|
||||
function inbucket.before.mail_accepted(localpart, domain)
|
||||
return localpart == "from" and domain == "test"
|
||||
if localpart == "from" and domain == "test" then
|
||||
logger.info("allowing message", {})
|
||||
return smtp.allow()
|
||||
else
|
||||
logger.info("denying message", {})
|
||||
return smtp.deny()
|
||||
end
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
_, err := luahost.NewFromReader(consoleLogger, extHost, strings.NewReader(script), "test.lua")
|
||||
_, err := luahost.NewFromReader(
|
||||
consoleLogger, extHost, strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send event to be accepted.
|
||||
addr := &event.AddressParts{Local: "from", Domain: "test"}
|
||||
got := extHost.Events.BeforeMailAccepted.Emit(addr)
|
||||
want := true
|
||||
want := event.ActionAllow
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
if *got != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
||||
if got.Action != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, addr)
|
||||
}
|
||||
|
||||
// Send event to be denied.
|
||||
addr = &event.AddressParts{Local: "reject", Domain: "me"}
|
||||
got = extHost.Events.BeforeMailAccepted.Emit(addr)
|
||||
want = false
|
||||
want = event.ActionDeny
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
if *got != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
||||
if got.Action != want {
|
||||
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func (lp *statePool) newState() (*lua.LState, error) {
|
||||
registerInbucketTypes(ls)
|
||||
registerMailAddressType(ls)
|
||||
registerMessageMetadataType(ls)
|
||||
registerPolicyType(ls)
|
||||
registerSMTPResponseType(ls)
|
||||
|
||||
// Run compiled script.
|
||||
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
||||
|
||||
@@ -446,10 +446,14 @@ func (s *Session) parseMailFromCmd(arg string) {
|
||||
}
|
||||
|
||||
// Process through extensions.
|
||||
extAction := event.ActionDefer
|
||||
extResult := s.extHost.Events.BeforeMailAccepted.Emit(
|
||||
&event.AddressParts{Local: localpart, Domain: domain})
|
||||
if extResult != nil && !*extResult {
|
||||
s.send("550 Mail denied by policy")
|
||||
if extResult != nil {
|
||||
extAction = extResult.Action
|
||||
}
|
||||
if extAction == event.ActionDeny {
|
||||
s.send(fmt.Sprintf("%03d %s", extResult.ErrorCode, extResult.ErrorMsg))
|
||||
s.logger.Warn().Msgf("Extension denied mail from <%v>", from)
|
||||
return
|
||||
}
|
||||
@@ -462,7 +466,8 @@ func (s *Session) parseMailFromCmd(arg string) {
|
||||
return
|
||||
}
|
||||
s.from = origin
|
||||
if !s.from.ShouldAccept() {
|
||||
// Ignore ShouldAccept if extensions explicitly allowed this From.
|
||||
if extAction == event.ActionDefer && !s.from.ShouldAccept() {
|
||||
s.send("501 Unauthorized domain")
|
||||
s.logger.Warn().Msgf("Bad domain sender %s", domain)
|
||||
return
|
||||
|
||||
@@ -393,9 +393,9 @@ func TestBeforeMailAcceptedEventEmitted(t *testing.T) {
|
||||
var got *event.AddressParts
|
||||
extHost.Events.BeforeMailAccepted.AddListener(
|
||||
"test",
|
||||
func(addr event.AddressParts) *bool {
|
||||
func(addr event.AddressParts) *event.SMTPResponse {
|
||||
got = &addr
|
||||
return nil
|
||||
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||
})
|
||||
|
||||
// Play and verify SMTP session.
|
||||
@@ -416,32 +416,34 @@ func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
||||
extHost := extension.NewHost()
|
||||
server := setupSMTPServer(ds, extHost)
|
||||
|
||||
var shouldReturn *bool
|
||||
var shouldReturn *event.SMTPResponse
|
||||
var gotEvent *event.AddressParts
|
||||
extHost.Events.BeforeMailAccepted.AddListener(
|
||||
"test",
|
||||
func(addr event.AddressParts) *bool {
|
||||
func(addr event.AddressParts) *event.SMTPResponse {
|
||||
gotEvent = &addr
|
||||
return shouldReturn
|
||||
})
|
||||
|
||||
allowRes := true
|
||||
denyRes := false
|
||||
tcs := map[string]struct {
|
||||
script scriptStep // Command to send and SMTP code expected.
|
||||
eventRes *bool // Response to send from event listener.
|
||||
script scriptStep // Command to send and SMTP code expected.
|
||||
eventRes event.SMTPResponse // Response to send from event listener.
|
||||
}{
|
||||
"allow": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||
eventRes: &allowRes,
|
||||
eventRes: event.SMTPResponse{Action: event.ActionAllow},
|
||||
},
|
||||
"deny": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 550},
|
||||
eventRes: &denyRes,
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 550},
|
||||
eventRes: event.SMTPResponse{
|
||||
Action: event.ActionDeny,
|
||||
ErrorCode: 550,
|
||||
ErrorMsg: "meh",
|
||||
},
|
||||
},
|
||||
"defer": {
|
||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||
eventRes: nil,
|
||||
eventRes: event.SMTPResponse{Action: event.ActionDefer},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -449,7 +451,7 @@ func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Reset event listener.
|
||||
shouldReturn = tc.eventRes
|
||||
shouldReturn = &tc.eventRes
|
||||
gotEvent = nil
|
||||
|
||||
// Play and verify SMTP session.
|
||||
|
||||
Reference in New Issue
Block a user