mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
feat: Add SMTPSession and BeforeRcptToAccepted event (#541)
Signed-off-by: James Hillyerd <james@hillyerd.com>
This commit is contained in:
@@ -47,3 +47,9 @@ type SMTPResponse struct {
|
||||
ErrorCode int // SMTP error code to respond with on deny.
|
||||
ErrorMsg string // SMTP error message to respond with on deny.
|
||||
}
|
||||
|
||||
// SMTPSession captures SMTP `MAIL FROM` & `RCPT TO` values prior to mail DATA being received.
|
||||
type SMTPSession struct {
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
}
|
||||
|
||||
@@ -20,10 +20,11 @@ type Host struct {
|
||||
// processed asynchronously with respect to the rest of Inbuckets operation. However, an event
|
||||
// listener will not be called until the one before it completes.
|
||||
type Events struct {
|
||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||
BeforeMailAccepted EventBroker[event.AddressParts, event.SMTPResponse]
|
||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||
BeforeMailAccepted EventBroker[event.AddressParts, event.SMTPResponse]
|
||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||
BeforeRcptToAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
|
||||
}
|
||||
|
||||
// Void indicates the event emitter will ignore any value returned by listeners.
|
||||
|
||||
@@ -29,8 +29,9 @@ type InbucketAfterFuncs struct {
|
||||
// InbucketBeforeFuncs holds references to Lua extension functions to be called
|
||||
// before Inbucket handles an event.
|
||||
type InbucketBeforeFuncs struct {
|
||||
MailAccepted *lua.LFunction
|
||||
MessageStored *lua.LFunction
|
||||
MailAccepted *lua.LFunction
|
||||
MessageStored *lua.LFunction
|
||||
RcptToAccepted *lua.LFunction
|
||||
}
|
||||
|
||||
func registerInbucketTypes(ls *lua.LState) {
|
||||
@@ -189,6 +190,8 @@ func inbucketBeforeIndex(ls *lua.LState) int {
|
||||
ls.Push(funcOrNil(before.MailAccepted))
|
||||
case "message_stored":
|
||||
ls.Push(funcOrNil(before.MessageStored))
|
||||
case "rcpt_to_accepted":
|
||||
ls.Push(funcOrNil(before.RcptToAccepted))
|
||||
default:
|
||||
// Unknown field.
|
||||
ls.Push(lua.LNil)
|
||||
@@ -207,6 +210,8 @@ func inbucketBeforeNewIndex(ls *lua.LState) int {
|
||||
m.MailAccepted = 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)
|
||||
}
|
||||
|
||||
70
pkg/extension/luahost/bind_smtpsession.go
Normal file
70
pkg/extension/luahost/bind_smtpsession.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const smtpSessionName = "smtp_session"
|
||||
|
||||
func registerSMTPSessionType(ls *lua.LState) {
|
||||
mt := ls.NewTypeMetatable(smtpSessionName)
|
||||
ls.SetGlobal(smtpSessionName, mt)
|
||||
|
||||
// Static attributes.
|
||||
ls.SetField(mt, "new", ls.NewFunction(newSMTPSession))
|
||||
|
||||
// Methods.
|
||||
ls.SetField(mt, "__index", ls.NewFunction(smtpSessionIndex))
|
||||
}
|
||||
|
||||
func newSMTPSession(ls *lua.LState) int {
|
||||
val := &event.SMTPSession{}
|
||||
ud := wrapSMTPSession(ls, val)
|
||||
ls.Push(ud)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func wrapSMTPSession(ls *lua.LState, val *event.SMTPSession) *lua.LUserData {
|
||||
ud := ls.NewUserData()
|
||||
ud.Value = val
|
||||
ls.SetMetatable(ud, ls.GetTypeMetatable(smtpSessionName))
|
||||
|
||||
return ud
|
||||
}
|
||||
|
||||
// Checks there is an SMTPSession at stack position `pos`, else throws Lua error.
|
||||
func checkSMTPSession(ls *lua.LState, pos int) *event.SMTPSession {
|
||||
ud := ls.CheckUserData(pos)
|
||||
if v, ok := ud.Value.(*event.SMTPSession); ok {
|
||||
return v
|
||||
}
|
||||
ls.ArgError(pos, smtpSessionName+" expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets a field value from SMTPSession user object. This emulates a Lua table,
|
||||
// allowing `msg.subject` instead of a Lua object syntax of `msg:subject()`.
|
||||
func smtpSessionIndex(ls *lua.LState) int {
|
||||
m := checkSMTPSession(ls, 1)
|
||||
field := ls.CheckString(2)
|
||||
|
||||
// Push the requested field's value onto the stack.
|
||||
switch field {
|
||||
case "from":
|
||||
ls.Push(wrapMailAddress(ls, m.From))
|
||||
case "to":
|
||||
lt := &lua.LTable{}
|
||||
for _, v := range m.To {
|
||||
addr := v
|
||||
lt.Append(wrapMailAddress(ls, addr))
|
||||
}
|
||||
ls.Push(lt)
|
||||
default:
|
||||
// Unknown field.
|
||||
ls.Push(lua.LNil)
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
38
pkg/extension/luahost/bind_smtpsession_test.go
Normal file
38
pkg/extension/luahost/bind_smtpsession_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package luahost
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"testing"
|
||||
|
||||
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSMTPSessionGetters(t *testing.T) {
|
||||
want := &event.SMTPSession{
|
||||
From: &mail.Address{Name: "name1", Address: "addr1"},
|
||||
To: []*mail.Address{
|
||||
{Name: "name2", Address: "addr2"},
|
||||
{Name: "name3", Address: "addr3"},
|
||||
},
|
||||
}
|
||||
script := `
|
||||
assert(msg, "msg should not be nil")
|
||||
|
||||
assert_eq(msg.from.name, "name1", "from.name")
|
||||
assert_eq(msg.from.address, "addr1", "from.address")
|
||||
|
||||
assert_eq(#msg.to, 2, "#msg.to")
|
||||
assert_eq(msg.to[1].name, "name2", "to[1].name")
|
||||
assert_eq(msg.to[1].address, "addr2", "to[1].address")
|
||||
assert_eq(msg.to[2].name, "name3", "to[2].name")
|
||||
assert_eq(msg.to[2].address, "addr3", "to[2].address")
|
||||
`
|
||||
|
||||
ls, _ := test.NewLuaState()
|
||||
registerSMTPSessionType(ls)
|
||||
registerMailAddressType(ls)
|
||||
ls.SetGlobal("msg", wrapSMTPSession(ls, want))
|
||||
require.NoError(t, ls.DoString(script))
|
||||
}
|
||||
@@ -112,6 +112,9 @@ func (h *Host) wireFunctions(logger zerolog.Logger, ls *lua.LState) {
|
||||
if ib.Before.MessageStored != nil {
|
||||
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
|
||||
}
|
||||
if ib.Before.RcptToAccepted != nil {
|
||||
events.BeforeRcptToAccepted.AddListener(listenerName, h.handleBeforeRcptToAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
|
||||
@@ -169,9 +172,33 @@ func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *event.SMTPResp
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
if lval.Type() == lua.LTNil || lua.LVIsFalse(lval) {
|
||||
result, err := unwrapSMTPResponse(lval)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Bad response from Lua Function")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeRcptToAccepted(session event.SMTPSession) *event.SMTPResponse {
|
||||
logger, ls, ib, ok := h.prepareInbucketFuncCall("before.rcpt_to_accepted")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defer h.pool.putState(ls)
|
||||
|
||||
logger.Debug().Msgf("Calling Lua function with %+v", session)
|
||||
if err := ls.CallByParam(
|
||||
lua.P{Fn: ib.Before.RcptToAccepted, NRet: 1, Protect: true},
|
||||
wrapSMTPSession(ls, &session),
|
||||
); err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to call Lua function")
|
||||
return nil
|
||||
}
|
||||
|
||||
lval := ls.Get(-1)
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
result, err := unwrapSMTPResponse(lval)
|
||||
if err != nil {
|
||||
@@ -201,7 +228,7 @@ func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.Inboun
|
||||
ls.Pop(1)
|
||||
logger.Debug().Msgf("Lua function returned %q (%v)", lval, lval.Type().String())
|
||||
|
||||
if lval.Type() == lua.LTNil || lua.LVIsFalse(lval) {
|
||||
if lua.LVIsFalse(lval) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -211,3 +211,85 @@ func TestBeforeMessageStored(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, want, got, "Response InboundMessage did not match")
|
||||
}
|
||||
|
||||
func TestBeforeMessageStoredNilReturn(t *testing.T) {
|
||||
// Event to send.
|
||||
msg := event.InboundMessage{
|
||||
Mailboxes: []string{"one", "two"},
|
||||
From: &mail.Address{Name: "From Name", Address: "from@example.com"},
|
||||
To: []*mail.Address{
|
||||
{Name: "To1 Name", Address: "to1@example.com"},
|
||||
{Name: "To2 Name", Address: "to2@example.com"},
|
||||
},
|
||||
Subject: "inbound subj",
|
||||
Size: 42,
|
||||
}
|
||||
|
||||
// Register lua event listener.
|
||||
script := `
|
||||
async = true
|
||||
|
||||
function inbucket.before.message_stored(msg)
|
||||
assert(msg)
|
||||
notify:send(asserts_ok)
|
||||
|
||||
-- Generate response.
|
||||
return nil
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
// Send event to be accepted.
|
||||
got := extHost.Events.BeforeMessageStored.Emit(&msg)
|
||||
require.Nil(t, got, "Expected nil result from Emit()")
|
||||
|
||||
// Verify Lua assertions passed.
|
||||
test.AssertNotified(t, notify)
|
||||
}
|
||||
|
||||
func TestBeforeRcptToAccepted(t *testing.T) {
|
||||
// Event to send.
|
||||
session := event.SMTPSession{
|
||||
From: &mail.Address{Name: "", Address: "from@example.com"},
|
||||
To: []*mail.Address{
|
||||
{Name: "", Address: "to1@example.com"},
|
||||
{Name: "", Address: "to2@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
// Register lua event listener.
|
||||
script := `
|
||||
async = true
|
||||
|
||||
function inbucket.before.rcpt_to_accepted(msg)
|
||||
-- Verify incoming values.
|
||||
assert_eq(msg.from.address, "from@example.com")
|
||||
assert_eq(2, #msg.to, "#msg.to")
|
||||
assert_eq(msg.to[1].address, "to1@example.com")
|
||||
assert_eq(msg.to[2].address, "to2@example.com")
|
||||
notify:send(asserts_ok)
|
||||
|
||||
return smtp.allow()
|
||||
end
|
||||
`
|
||||
extHost := extension.NewHost()
|
||||
luaHost, err := luahost.NewFromReader(consoleLogger, extHost,
|
||||
strings.NewReader(test.LuaInit+script), "test.lua")
|
||||
require.NoError(t, err)
|
||||
notify := luaHost.CreateChannel("notify")
|
||||
|
||||
// Send event to be accepted.
|
||||
got := extHost.Events.BeforeRcptToAccepted.Emit(&session)
|
||||
require.NotNil(t, got, "Expected result from Emit()")
|
||||
|
||||
// Verify Lua assertions passed.
|
||||
test.AssertNotified(t, notify)
|
||||
|
||||
// Verify response values.
|
||||
want := event.SMTPResponse{Action: event.ActionAllow}
|
||||
assert.Equal(t, want, *got)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ func (lp *statePool) newState() (*lua.LState, error) {
|
||||
registerMailAddressType(ls)
|
||||
registerMessageMetadataType(ls)
|
||||
registerSMTPResponseType(ls)
|
||||
registerSMTPSessionType(ls)
|
||||
|
||||
// Run compiled script.
|
||||
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
||||
|
||||
Reference in New Issue
Block a user