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.
|
ErrorCode int // SMTP error code to respond with on deny.
|
||||||
ErrorMsg string // SMTP error message 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Events struct {
|
|||||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||||
BeforeMailAccepted EventBroker[event.AddressParts, event.SMTPResponse]
|
BeforeMailAccepted EventBroker[event.AddressParts, event.SMTPResponse]
|
||||||
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage]
|
||||||
|
BeforeRcptToAccepted EventBroker[event.SMTPSession, event.SMTPResponse]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Void indicates the event emitter will ignore any value returned by listeners.
|
// Void indicates the event emitter will ignore any value returned by listeners.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type InbucketAfterFuncs struct {
|
|||||||
type InbucketBeforeFuncs struct {
|
type InbucketBeforeFuncs struct {
|
||||||
MailAccepted *lua.LFunction
|
MailAccepted *lua.LFunction
|
||||||
MessageStored *lua.LFunction
|
MessageStored *lua.LFunction
|
||||||
|
RcptToAccepted *lua.LFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerInbucketTypes(ls *lua.LState) {
|
func registerInbucketTypes(ls *lua.LState) {
|
||||||
@@ -189,6 +190,8 @@ func inbucketBeforeIndex(ls *lua.LState) int {
|
|||||||
ls.Push(funcOrNil(before.MailAccepted))
|
ls.Push(funcOrNil(before.MailAccepted))
|
||||||
case "message_stored":
|
case "message_stored":
|
||||||
ls.Push(funcOrNil(before.MessageStored))
|
ls.Push(funcOrNil(before.MessageStored))
|
||||||
|
case "rcpt_to_accepted":
|
||||||
|
ls.Push(funcOrNil(before.RcptToAccepted))
|
||||||
default:
|
default:
|
||||||
// Unknown field.
|
// Unknown field.
|
||||||
ls.Push(lua.LNil)
|
ls.Push(lua.LNil)
|
||||||
@@ -207,6 +210,8 @@ func inbucketBeforeNewIndex(ls *lua.LState) int {
|
|||||||
m.MailAccepted = ls.CheckFunction(3)
|
m.MailAccepted = ls.CheckFunction(3)
|
||||||
case "message_stored":
|
case "message_stored":
|
||||||
m.MessageStored = ls.CheckFunction(3)
|
m.MessageStored = ls.CheckFunction(3)
|
||||||
|
case "rcpt_to_accepted":
|
||||||
|
m.RcptToAccepted = ls.CheckFunction(3)
|
||||||
default:
|
default:
|
||||||
ls.RaiseError("invalid inbucket.before index %q", index)
|
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 {
|
if ib.Before.MessageStored != nil {
|
||||||
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
|
events.BeforeMessageStored.AddListener(listenerName, h.handleBeforeMessageStored)
|
||||||
}
|
}
|
||||||
|
if ib.Before.RcptToAccepted != nil {
|
||||||
|
events.BeforeRcptToAccepted.AddListener(listenerName, h.handleBeforeRcptToAccepted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
|
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
|
||||||
@@ -169,9 +172,33 @@ func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *event.SMTPResp
|
|||||||
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 || 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
|
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)
|
result, err := unwrapSMTPResponse(lval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,7 +228,7 @@ func (h *Host) handleBeforeMessageStored(msg event.InboundMessage) *event.Inboun
|
|||||||
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 || lua.LVIsFalse(lval) {
|
if lua.LVIsFalse(lval) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,3 +211,85 @@ func TestBeforeMessageStored(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, want, got, "Response InboundMessage did not match")
|
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)
|
registerMailAddressType(ls)
|
||||||
registerMessageMetadataType(ls)
|
registerMessageMetadataType(ls)
|
||||||
registerSMTPResponseType(ls)
|
registerSMTPResponseType(ls)
|
||||||
|
registerSMTPSessionType(ls)
|
||||||
|
|
||||||
// Run compiled script.
|
// Run compiled script.
|
||||||
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
ls.Push(ls.NewFunctionFromProto(lp.funcProto))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/mail"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -495,7 +496,26 @@ func (s *Session) mailHandler(cmd string, arg string) {
|
|||||||
s.logger.Warn().Str("to", addr).Err(err).Msg("Bad address as RCPT arg")
|
s.logger.Warn().Str("to", addr).Err(err).Msg("Bad address as RCPT arg")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !recip.ShouldAccept() {
|
|
||||||
|
// Append new address to extSession for inspection.
|
||||||
|
addrCopy := recip.Address
|
||||||
|
extSession := s.extSession()
|
||||||
|
extSession.To = append(extSession.To, &addrCopy)
|
||||||
|
|
||||||
|
// Process through extensions.
|
||||||
|
extAction := event.ActionDefer
|
||||||
|
extResult := s.extHost.Events.BeforeRcptToAccepted.Emit(extSession)
|
||||||
|
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 to <%v>", recip.Address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore ShouldAccept if extensions explicitly allowed this Recipient.
|
||||||
|
if extAction == event.ActionDefer && !recip.ShouldAccept() {
|
||||||
s.logger.Warn().Str("to", addr).Msg("Rejecting recipient domain")
|
s.logger.Warn().Str("to", addr).Msg("Rejecting recipient domain")
|
||||||
s.send("550 Relay not permitted")
|
s.send("550 Relay not permitted")
|
||||||
return
|
return
|
||||||
@@ -687,3 +707,18 @@ func (s *Session) ooSeq(cmd string) {
|
|||||||
s.send(fmt.Sprintf("503 Command %v is out of sequence", cmd))
|
s.send(fmt.Sprintf("503 Command %v is out of sequence", cmd))
|
||||||
s.logger.Warn().Msgf("Wasn't expecting %v here", cmd)
|
s.logger.Warn().Msgf("Wasn't expecting %v here", cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extSession builds an SMTPSession for extensions.
|
||||||
|
func (s *Session) extSession() *event.SMTPSession {
|
||||||
|
from := s.from.Address
|
||||||
|
to := make([]*mail.Address, 0, len(s.recipients))
|
||||||
|
for _, recip := range s.recipients {
|
||||||
|
addr := recip.Address
|
||||||
|
to = append(to, &addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &event.SMTPSession{
|
||||||
|
From: &from,
|
||||||
|
To: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/inbucket/inbucket/v3/pkg/test"
|
"github.com/inbucket/inbucket/v3/pkg/test"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type scriptStep struct {
|
type scriptStep struct {
|
||||||
@@ -457,7 +458,7 @@ func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
|||||||
// Play and verify SMTP session.
|
// Play and verify SMTP session.
|
||||||
script := []scriptStep{
|
script := []scriptStep{
|
||||||
{"HELO localhost", 250},
|
{"HELO localhost", 250},
|
||||||
tc.script,
|
tc.script, // error code is the significant part.
|
||||||
{"QUIT", 221}}
|
{"QUIT", 221}}
|
||||||
playSession(t, server, script)
|
playSession(t, server, script)
|
||||||
|
|
||||||
@@ -466,6 +467,175 @@ func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests "RCPT TO" emits BeforeRcptToAccepted event.
|
||||||
|
func TestBeforeRcptToAcceptedSingleEventEmitted(t *testing.T) {
|
||||||
|
ds := test.NewStore()
|
||||||
|
extHost := extension.NewHost()
|
||||||
|
server := setupSMTPServer(ds, extHost)
|
||||||
|
|
||||||
|
var got *event.SMTPSession
|
||||||
|
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||||
|
"test",
|
||||||
|
func(session event.SMTPSession) *event.SMTPResponse {
|
||||||
|
got = &session
|
||||||
|
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Play and verify SMTP session.
|
||||||
|
script := []scriptStep{
|
||||||
|
{"HELO localhost", 250},
|
||||||
|
{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
|
{"RCPT TO:<user@gmail.com>", 250},
|
||||||
|
{"QUIT", 221}}
|
||||||
|
playSession(t, server, script)
|
||||||
|
|
||||||
|
require.NotNil(t, got, "BeforeRcptToListener did not receive SMTPSession")
|
||||||
|
require.NotNil(t, got.From)
|
||||||
|
require.NotNil(t, got.To)
|
||||||
|
assert.Equal(t, "john@gmail.com", got.From.Address)
|
||||||
|
assert.Len(t, got.To, 1)
|
||||||
|
assert.Equal(t, "user@gmail.com", got.To[0].Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests "RCPT TO" emits many BeforeRcptToAccepted events.
|
||||||
|
func TestBeforeRcptToAcceptedManyEventsEmitted(t *testing.T) {
|
||||||
|
ds := test.NewStore()
|
||||||
|
extHost := extension.NewHost()
|
||||||
|
server := setupSMTPServer(ds, extHost)
|
||||||
|
|
||||||
|
var called int
|
||||||
|
var got *event.SMTPSession
|
||||||
|
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||||
|
"test",
|
||||||
|
func(session event.SMTPSession) *event.SMTPResponse {
|
||||||
|
called++
|
||||||
|
got = &session
|
||||||
|
return &event.SMTPResponse{Action: event.ActionDefer}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Play and verify SMTP session.
|
||||||
|
script := []scriptStep{
|
||||||
|
{"HELO localhost", 250},
|
||||||
|
{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
|
{"RCPT TO:<user@gmail.com>", 250},
|
||||||
|
{"RCPT TO:<user2@gmail.com>", 250},
|
||||||
|
{"QUIT", 221}}
|
||||||
|
playSession(t, server, script)
|
||||||
|
|
||||||
|
require.Equal(t, 2, called, "2 events should have been emitted")
|
||||||
|
require.NotNil(t, got, "BeforeRcptToListener did not receive SMTPSession")
|
||||||
|
require.NotNil(t, got.From)
|
||||||
|
require.NotNil(t, got.To)
|
||||||
|
assert.Equal(t, "john@gmail.com", got.From.Address)
|
||||||
|
assert.Len(t, got.To, 2)
|
||||||
|
assert.Equal(t, "user@gmail.com", got.To[0].Address)
|
||||||
|
assert.Equal(t, "user2@gmail.com", got.To[1].Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests we can continue after denying a "RCPT TO".
|
||||||
|
func TestBeforeRcptToAcceptedEventDeny(t *testing.T) {
|
||||||
|
ds := test.NewStore()
|
||||||
|
extHost := extension.NewHost()
|
||||||
|
server := setupSMTPServer(ds, extHost)
|
||||||
|
|
||||||
|
var called int
|
||||||
|
var got *event.SMTPSession
|
||||||
|
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||||
|
"test",
|
||||||
|
func(session event.SMTPSession) *event.SMTPResponse {
|
||||||
|
called++
|
||||||
|
|
||||||
|
// Reject bad address.
|
||||||
|
action := event.ActionDefer
|
||||||
|
for _, to := range session.To {
|
||||||
|
if to.Address == "bad@apple.com" {
|
||||||
|
action = event.ActionDeny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
got = &session
|
||||||
|
return &event.SMTPResponse{Action: action, ErrorCode: 550, ErrorMsg: "rotten"}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Play and verify SMTP session.
|
||||||
|
script := []scriptStep{
|
||||||
|
{"HELO localhost", 250},
|
||||||
|
{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
|
{"RCPT TO:<user@gmail.com>", 250},
|
||||||
|
{"RCPT TO:<bad@apple.com>", 550},
|
||||||
|
{"RCPT TO:<user2@gmail.com>", 250},
|
||||||
|
{"QUIT", 221}}
|
||||||
|
playSession(t, server, script)
|
||||||
|
|
||||||
|
require.Equal(t, 3, called, "3 events should have been emitted")
|
||||||
|
require.NotNil(t, got, "BeforeRcptToListener did not receive SMTPSession")
|
||||||
|
require.NotNil(t, got.From)
|
||||||
|
require.NotNil(t, got.To)
|
||||||
|
assert.Equal(t, "john@gmail.com", got.From.Address)
|
||||||
|
|
||||||
|
// Verify bad apple dropped from final event.
|
||||||
|
assert.Len(t, got.To, 2)
|
||||||
|
assert.Equal(t, "user@gmail.com", got.To[0].Address)
|
||||||
|
assert.Equal(t, "user2@gmail.com", got.To[1].Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test "RCPT TO" acts on BeforeRcptToAccepted event result.
|
||||||
|
func TestBeforeRcptToAcceptedEventResponse(t *testing.T) {
|
||||||
|
ds := test.NewStore()
|
||||||
|
extHost := extension.NewHost()
|
||||||
|
server := setupSMTPServer(ds, extHost)
|
||||||
|
|
||||||
|
var shouldReturn *event.SMTPResponse
|
||||||
|
var gotEvent *event.SMTPSession
|
||||||
|
extHost.Events.BeforeRcptToAccepted.AddListener(
|
||||||
|
"test",
|
||||||
|
func(session event.SMTPSession) *event.SMTPResponse {
|
||||||
|
gotEvent = &session
|
||||||
|
return shouldReturn
|
||||||
|
})
|
||||||
|
|
||||||
|
tcs := map[string]struct {
|
||||||
|
script scriptStep // Command to send and SMTP code expected.
|
||||||
|
eventRes event.SMTPResponse // Response to send from event listener.
|
||||||
|
}{
|
||||||
|
"allow": {
|
||||||
|
script: scriptStep{"RCPT TO:<john@gmail.com>", 250},
|
||||||
|
eventRes: event.SMTPResponse{Action: event.ActionAllow},
|
||||||
|
},
|
||||||
|
"deny": {
|
||||||
|
script: scriptStep{"RCPT TO:<john@gmail.com>", 550},
|
||||||
|
eventRes: event.SMTPResponse{
|
||||||
|
Action: event.ActionDeny,
|
||||||
|
ErrorCode: 550,
|
||||||
|
ErrorMsg: "meh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"defer": {
|
||||||
|
script: scriptStep{"RCPT TO:<john@gmail.com>", 250},
|
||||||
|
eventRes: event.SMTPResponse{Action: event.ActionDefer},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tcs {
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// Reset event listener.
|
||||||
|
shouldReturn = &tc.eventRes
|
||||||
|
gotEvent = nil
|
||||||
|
|
||||||
|
// Play and verify SMTP session.
|
||||||
|
script := []scriptStep{
|
||||||
|
{"HELO localhost", 250},
|
||||||
|
{"MAIL FROM:<user@gmail.com>", 250},
|
||||||
|
tc.script, // error code is the significant part.
|
||||||
|
{"QUIT", 221}}
|
||||||
|
playSession(t, server, script)
|
||||||
|
|
||||||
|
assert.NotNil(t, gotEvent, "BeforeRcptToListener did not receive SMTPSession")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// playSession creates a new session, reads the greeting and then plays the script
|
// playSession creates a new session, reads the greeting and then plays the script
|
||||||
func playSession(t *testing.T, server *Server, script []scriptStep) {
|
func playSession(t *testing.T, server *Server, script []scriptStep) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
Reference in New Issue
Block a user