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:
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"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")
|
||||
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.send("550 Relay not permitted")
|
||||
return
|
||||
@@ -687,3 +707,18 @@ func (s *Session) ooSeq(cmd string) {
|
||||
s.send(fmt.Sprintf("503 Command %v is out of sequence", 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/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type scriptStep struct {
|
||||
@@ -457,7 +458,7 @@ func TestBeforeMailAcceptedEventResponse(t *testing.T) {
|
||||
// Play and verify SMTP session.
|
||||
script := []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
tc.script,
|
||||
tc.script, // error code is the significant part.
|
||||
{"QUIT", 221}}
|
||||
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
|
||||
func playSession(t *testing.T, server *Server, script []scriptStep) {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user