1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 01:27:01 +00:00

feat: Add SMTPResponse type for extensions (#539)

Signed-off-by: James Hillyerd <james@hillyerd.com>
This commit is contained in:
James Hillyerd
2024-10-05 18:16:49 -07:00
committed by GitHub
parent 8097b3cc8a
commit 3110183a17
11 changed files with 165 additions and 49 deletions

9
.luarc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"runtime.version": "Lua 5.1",
"diagnostics": {
"globals": [
"inbucket",
"smtp"
]
}
}

View File

@@ -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.
}

View File

@@ -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]
}

View File

@@ -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)
}

View 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())
}

View 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",
})
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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))

View File

@@ -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

View File

@@ -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.