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:
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"
|
"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.
|
// AddressParts contains the local and domain parts of an email address.
|
||||||
type AddressParts struct {
|
type AddressParts struct {
|
||||||
Local string
|
Local string
|
||||||
@@ -31,3 +40,10 @@ type MessageMetadata struct {
|
|||||||
Size int64
|
Size int64
|
||||||
Seen bool
|
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 {
|
type Events struct {
|
||||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||||
BeforeMailAccepted EventBroker[event.AddressParts, bool]
|
BeforeMailAccepted EventBroker[event.AddressParts, event.SMTPResponse]
|
||||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
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")
|
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.mail_accepted")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@@ -169,16 +169,16 @@ func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *bool {
|
|||||||
ls.Pop(1)
|
ls.Pop(1)
|
||||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := true
|
result, err := unwrapSMTPResponse(lval)
|
||||||
if lua.LVIsFalse(lval) {
|
if err != nil {
|
||||||
result = false
|
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.InboundMessage {
|
func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.InboundMessage {
|
||||||
|
|||||||
@@ -109,29 +109,36 @@ func TestBeforeMailAccepted(t *testing.T) {
|
|||||||
// Register lua event listener.
|
// Register lua event listener.
|
||||||
script := `
|
script := `
|
||||||
function inbucket.before.mail_accepted(localpart, domain)
|
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
|
end
|
||||||
`
|
`
|
||||||
extHost := extension.NewHost()
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Send event to be accepted.
|
// Send event to be accepted.
|
||||||
addr := &event.AddressParts{Local: "from", Domain: "test"}
|
addr := &event.AddressParts{Local: "from", Domain: "test"}
|
||||||
got := extHost.Events.BeforeMailAccepted.Emit(addr)
|
got := extHost.Events.BeforeMailAccepted.Emit(addr)
|
||||||
want := true
|
want := event.ActionAllow
|
||||||
require.NotNil(t, got, "Expected result from Emit()")
|
require.NotNil(t, got, "Expected result from Emit()")
|
||||||
if *got != want {
|
if got.Action != want {
|
||||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
t.Errorf("Got %v, wanted %v for addr %v", got.Action, want, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send event to be denied.
|
// Send event to be denied.
|
||||||
addr = &event.AddressParts{Local: "reject", Domain: "me"}
|
addr = &event.AddressParts{Local: "reject", Domain: "me"}
|
||||||
got = extHost.Events.BeforeMailAccepted.Emit(addr)
|
got = extHost.Events.BeforeMailAccepted.Emit(addr)
|
||||||
want = false
|
want = event.ActionDeny
|
||||||
require.NotNil(t, got, "Expected result from Emit()")
|
require.NotNil(t, got, "Expected result from Emit()")
|
||||||
if *got != want {
|
if got.Action != want {
|
||||||
t.Errorf("Got %v, wanted %v for addr %v", *got, want, addr)
|
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)
|
registerInbucketTypes(ls)
|
||||||
registerMailAddressType(ls)
|
registerMailAddressType(ls)
|
||||||
registerMessageMetadataType(ls)
|
registerMessageMetadataType(ls)
|
||||||
registerPolicyType(ls)
|
registerSMTPResponseType(ls)
|
||||||
|
|
||||||
// Run compiled script.
|
// Run compiled script.
|
||||||
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
||||||
|
|||||||
@@ -446,10 +446,14 @@ func (s *Session) parseMailFromCmd(arg string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process through extensions.
|
// Process through extensions.
|
||||||
|
extAction := event.ActionDefer
|
||||||
extResult := s.extHost.Events.BeforeMailAccepted.Emit(
|
extResult := s.extHost.Events.BeforeMailAccepted.Emit(
|
||||||
&event.AddressParts{Local: localpart, Domain: domain})
|
&event.AddressParts{Local: localpart, Domain: domain})
|
||||||
if extResult != nil && !*extResult {
|
if extResult != nil {
|
||||||
s.send("550 Mail denied by policy")
|
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)
|
s.logger.Warn().Msgf("Extension denied mail from <%v>", from)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -462,7 +466,8 @@ func (s *Session) parseMailFromCmd(arg string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.from = origin
|
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.send("501 Unauthorized domain")
|
||||||
s.logger.Warn().Msgf("Bad domain sender %s", domain)
|
s.logger.Warn().Msgf("Bad domain sender %s", domain)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -393,9 +393,9 @@ func TestBeforeMailAcceptedEventEmitted(t *testing.T) {
|
|||||||
var got *event.AddressParts
|
var got *event.AddressParts
|
||||||
extHost.Events.BeforeMailAccepted.AddListener(
|
extHost.Events.BeforeMailAccepted.AddListener(
|
||||||
"test",
|
"test",
|
||||||
func(addr event.AddressParts) *bool {
|
func(addr event.AddressParts) *event.SMTPResponse {
|
||||||
got = &addr
|
got = &addr
|
||||||
return nil
|
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Play and verify SMTP session.
|
// Play and verify SMTP session.
|
||||||
@@ -416,32 +416,34 @@ func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
|||||||
extHost := extension.NewHost()
|
extHost := extension.NewHost()
|
||||||
server := setupSMTPServer(ds, extHost)
|
server := setupSMTPServer(ds, extHost)
|
||||||
|
|
||||||
var shouldReturn *bool
|
var shouldReturn *event.SMTPResponse
|
||||||
var gotEvent *event.AddressParts
|
var gotEvent *event.AddressParts
|
||||||
extHost.Events.BeforeMailAccepted.AddListener(
|
extHost.Events.BeforeMailAccepted.AddListener(
|
||||||
"test",
|
"test",
|
||||||
func(addr event.AddressParts) *bool {
|
func(addr event.AddressParts) *event.SMTPResponse {
|
||||||
gotEvent = &addr
|
gotEvent = &addr
|
||||||
return shouldReturn
|
return shouldReturn
|
||||||
})
|
})
|
||||||
|
|
||||||
allowRes := true
|
|
||||||
denyRes := false
|
|
||||||
tcs := map[string]struct {
|
tcs := map[string]struct {
|
||||||
script scriptStep // Command to send and SMTP code expected.
|
script scriptStep // Command to send and SMTP code expected.
|
||||||
eventRes *bool // Response to send from event listener.
|
eventRes event.SMTPResponse // Response to send from event listener.
|
||||||
}{
|
}{
|
||||||
"allow": {
|
"allow": {
|
||||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
eventRes: &allowRes,
|
eventRes: event.SMTPResponse{Action: event.ActionAllow},
|
||||||
},
|
},
|
||||||
"deny": {
|
"deny": {
|
||||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 550},
|
script: scriptStep{"MAIL FROM:<john@gmail.com>", 550},
|
||||||
eventRes: &denyRes,
|
eventRes: event.SMTPResponse{
|
||||||
|
Action: event.ActionDeny,
|
||||||
|
ErrorCode: 550,
|
||||||
|
ErrorMsg: "meh",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"defer": {
|
"defer": {
|
||||||
script: scriptStep{"MAIL FROM:<john@gmail.com>", 250},
|
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
|
tc := tc
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
// Reset event listener.
|
// Reset event listener.
|
||||||
shouldReturn = tc.eventRes
|
shouldReturn = &tc.eventRes
|
||||||
gotEvent = nil
|
gotEvent = nil
|
||||||
|
|
||||||
// Play and verify SMTP session.
|
// Play and verify SMTP session.
|
||||||
|
|||||||
Reference in New Issue
Block a user