From 01fb161df8f3e5e118a1db8a20e4e67189722a12 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Mon, 6 Nov 2023 14:53:38 -0800 Subject: [PATCH] extension: BeforeMessageStored event to rewrite envelope (#417) * extension: add InboundMessage type * manager: fires BeforeMessageStored event * manager: Reacts to BeforeMessageStored event response * manager: Apply BeforeMessageStored response fields to message Signed-off-by: James Hillyerd --- pkg/extension/broker.go | 2 +- pkg/extension/event/events.go | 9 ++ pkg/extension/host.go | 1 + pkg/message/manager.go | 86 +++++++++---- pkg/message/manager_test.go | 168 ++++++++++++++++++++++++- pkg/server/smtp/handler_test.go | 2 +- pkg/test/testdata/basic.golden | 2 +- pkg/test/testdata/encodedheader.golden | 2 +- pkg/test/testdata/fullname.golden | 2 +- pkg/test/testdata/no-to-ipv4.golden | 2 +- pkg/test/testdata/no-to-ipv6.golden | 2 +- 11 files changed, 244 insertions(+), 34 deletions(-) diff --git a/pkg/extension/broker.go b/pkg/extension/broker.go index 742482e..e76a0e9 100644 --- a/pkg/extension/broker.go +++ b/pkg/extension/broker.go @@ -6,7 +6,7 @@ import ( // EventBroker maintains a list of listeners interested in a specific type // of event. -type EventBroker[E any, R comparable] struct { +type EventBroker[E any, R interface{}] struct { sync.RWMutex listenerNames []string // Ordered listener names. listenerFuncs []func(E) *R // Ordered listener functions. diff --git a/pkg/extension/event/events.go b/pkg/extension/event/events.go index 429e9d5..554a8a7 100644 --- a/pkg/extension/event/events.go +++ b/pkg/extension/event/events.go @@ -11,6 +11,15 @@ type AddressParts struct { Domain string } +// InboundMessage contains the basic header and mailbox data for a message being received. +type InboundMessage struct { + Mailboxes []string + From mail.Address + To []mail.Address + Subject string + Size int64 +} + // MessageMetadata contains the basic header data for a message event. type MessageMetadata struct { Mailbox string diff --git a/pkg/extension/host.go b/pkg/extension/host.go index eb910eb..2832744 100644 --- a/pkg/extension/host.go +++ b/pkg/extension/host.go @@ -23,6 +23,7 @@ type Events struct { AfterMessageDeleted AsyncEventBroker[event.MessageMetadata] AfterMessageStored AsyncEventBroker[event.MessageMetadata] BeforeMailAccepted EventBroker[event.AddressParts, bool] + BeforeMessageStored EventBroker[event.InboundMessage, event.InboundMessage] } // Void indicates the event emitter will ignore any value returned by listeners. diff --git a/pkg/message/manager.go b/pkg/message/manager.go index 28d8001..25f748b 100644 --- a/pkg/message/manager.go +++ b/pkg/message/manager.go @@ -69,39 +69,73 @@ func (s *StoreManager) Deliver( toaddr[i] = &torecip.Address } } + subject := header.Get("Subject") now := time.Now() tstamp := now.UTC().Format(recvdTimeFmt) - // Deliver to mailboxes. - for _, recip := range recipients { - if recip.ShouldStore() { - // Append recipient and timestamp to generated Recieved header. - recvd := fmt.Sprintf("%s for <%s>; %s\r\n", recvdHeader, recip.Address.Address, tstamp) + // Process inbound message through extensions. + mailboxes := make([]string, len(recipients)) + toAddrs := make([]mail.Address, len(recipients)) + for i, recip := range recipients { + mailboxes[i] = recip.Mailbox + toAddrs[i] = recip.Address + } - // Deliver message. - logger.Debug().Str("mailbox", recip.Mailbox).Msg("Delivering message") - delivery := &Delivery{ - Meta: event.MessageMetadata{ - Mailbox: recip.Mailbox, - From: fromaddr[0], - To: toaddr, - Date: now, - Subject: header.Get("Subject"), - }, - Reader: io.MultiReader(strings.NewReader(recvd), bytes.NewReader(source)), - } - id, err := s.Store.AddMessage(delivery) - if err != nil { - logger.Error().Str("mailbox", recip.Mailbox).Err(err).Msg("Delivery failed") - return err - } + inbound := &event.InboundMessage{ + Mailboxes: mailboxes, + From: *fromaddr[0], + To: toAddrs, + Subject: subject, + Size: int64(len(source)), + } - // Emit message stored event. - event := delivery.Meta - event.ID = id - s.ExtHost.Events.AfterMessageStored.Emit(&event) + extResult := s.ExtHost.Events.BeforeMessageStored.Emit(inbound) + if extResult == nil { + // Use address policy to determine deliverable mailboxes. + mailboxes = mailboxes[:0] + for _, recip := range recipients { + if recip.ShouldStore() { + mailboxes = append(mailboxes, recip.Mailbox) + } } + inbound.Mailboxes = mailboxes + } else { + // Event response overrides destination mailboxes and address policy. + inbound = extResult + toaddr = make([]*mail.Address, len(inbound.To)) + for i := range inbound.To { + toaddr[i] = &inbound.To[i] + } + } + + // Deliver to mailboxes. + for _, mb := range inbound.Mailboxes { + // Append recipient and timestamp to generated Recieved header. + recvd := fmt.Sprintf("%s for <%s>; %s\r\n", recvdHeader, mb, tstamp) + + // Deliver message. + logger.Debug().Str("mailbox", mb).Msg("Delivering message") + delivery := &Delivery{ + Meta: event.MessageMetadata{ + Mailbox: mb, + From: &inbound.From, + To: toaddr, + Date: now, + Subject: inbound.Subject, + }, + Reader: io.MultiReader(strings.NewReader(recvd), bytes.NewReader(source)), + } + id, err := s.Store.AddMessage(delivery) + if err != nil { + logger.Error().Str("mailbox", mb).Err(err).Msg("Delivery failed") + return err + } + + // Emit message stored event. + event := delivery.Meta + event.ID = id + s.ExtHost.Events.AfterMessageStored.Emit(&event) } return nil diff --git a/pkg/message/manager_test.go b/pkg/message/manager_test.go index 7559aee..e2959ef 100644 --- a/pkg/message/manager_test.go +++ b/pkg/message/manager_test.go @@ -1,10 +1,12 @@ package message_test import ( + "net/mail" "testing" "github.com/inbucket/inbucket/v3/pkg/config" "github.com/inbucket/inbucket/v3/pkg/extension" + "github.com/inbucket/inbucket/v3/pkg/extension/event" "github.com/inbucket/inbucket/v3/pkg/message" "github.com/inbucket/inbucket/v3/pkg/policy" "github.com/inbucket/inbucket/v3/pkg/test" @@ -48,16 +50,140 @@ func TestDeliverRespectsRecipientPolicy(t *testing.T) { t.Fatal(err) } + // Expect empty mailbox for nostore domain. assertMessageCount(t, sm, "u1@nostore.com", 0) assertMessageCount(t, sm, "u2@example.com", 1) } +func TestDeliverEmitsBeforeMessageStoredEvent(t *testing.T) { + sm, extHost := testStoreManager() + + // Register function to receive event. + var got *event.InboundMessage + extHost.Events.BeforeMessageStored.AddListener( + "test", + func(msg event.InboundMessage) *event.InboundMessage { + got = &msg + return nil + }) + + // Deliver a message to trigger event. + origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com") + recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com") + recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com") + if err := sm.Deliver( + origin, + []*policy.Recipient{recip1, recip2}, + "Received: xyz\n", + []byte("From: from@example.com\nSubject: tsub\n\ntest email"), + ); err != nil { + t.Fatal(err) + } + + require.NotNil(t, got, "BeforeMessageStored listener did not receive InboundMessage") + assert.Equal(t, []string{"u1@example.com", "u2@example.com"}, got.Mailboxes, "Mailboxes not equal") + assert.Equal(t, mail.Address{Name: "", Address: "from@example.com"}, got.From, "From not equal") + assert.Equal(t, []mail.Address{ + {Name: "", Address: "u1@example.com"}, + {Name: "", Address: "u2@example.com"}, + }, got.To, "To not equal") + assert.Equal(t, "tsub", got.Subject, "Subject not equal") + assert.Equal(t, int64(48), got.Size, "Size not equal") +} + +func TestDeliverUsesBeforeMessageStoredEventResponseMailboxes(t *testing.T) { + sm, extHost := testStoreManager() + + // Register function to receive event. + extHost.Events.BeforeMessageStored.AddListener( + "test", + func(msg event.InboundMessage) *event.InboundMessage { + // Listener rewrites destination mailboxes. + resp := msg + resp.Mailboxes = []string{"new1@example.com", "new2@nostore.com"} + return &resp + }) + + // Deliver a message to trigger event. + origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com") + recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com") + recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com") + if err := sm.Deliver( + origin, + []*policy.Recipient{recip1, recip2}, + "Received: xyz\r\n", + []byte("From: from@example.com\nSubject: tsub\n\ntest email"), + ); err != nil { + t.Fatal(err) + } + + // Expect messages in only the mailboxes in the event response, and for the DiscardDomains + // policy to be ignored. + assertMessageCount(t, sm, "u1@example.com", 0) + assertMessageCount(t, sm, "u2@example.com", 0) + assertMessageCount(t, sm, "new1@example.com", 1) + assertMessageCount(t, sm, "new2@nostore.com", 1) +} + +func TestDeliverUsesBeforeMessageStoredEventResponseFields(t *testing.T) { + sm, extHost := testStoreManager() + + // Register function to receive event. + extHost.Events.BeforeMessageStored.AddListener( + "test", + func(msg event.InboundMessage) *event.InboundMessage { + // Listener rewrites destination mailboxes. + msg.Subject = "event subj" + msg.From = mail.Address{Address: "from@event.com", Name: "From Event"} + + // Changing To does not affect destination mailbox(es). + msg.To = []mail.Address{ + {Address: "to@event.com", Name: "To Event"}, + {Address: "to2@event.com", Name: "To 2 Event"}, + } + + // Size is read only, should have no effect. + msg.Size = 12345 + + return &msg + }) + + // Deliver a message to trigger event. + origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com") + recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com") + if err := sm.Deliver( + origin, + []*policy.Recipient{recip1}, + "Received: xyz\r\n", + []byte("From: from@example.com\nSubject: tsub\n\ntest email"), + ); err != nil { + t.Fatal(err) + } + + // Verify single message stored. + metadata, err := sm.GetMetadata("u1@example.com") + require.NoError(t, err) + require.Len(t, metadata, 1, "mailbox has incorrect # of messages") + got := metadata[0] + + // Verify metadata fields were overridden by event response values. + assert.Equal(t, "event subj", got.Subject, "Subject didn't match") + assert.Equal(t, "from@event.com", got.From.Address, "From Address didn't match") + assert.Equal(t, "From Event", got.From.Name, "From Name didn't match") + require.Len(t, got.To, 2) + assert.Equal(t, "to@event.com", got.To[0].Address, "To Address didn't match") + assert.Equal(t, "To Event", got.To[0].Name, "To Name didn't match") + assert.Equal(t, "to2@event.com", got.To[1].Address, "To Address didn't match") + assert.Equal(t, "To 2 Event", got.To[1].Name, "To Name didn't match") + assert.NotEqual(t, 12345, got.Size, "Size is read only") +} + func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) { sm, extHost := testStoreManager() listener := extHost.Events.AfterMessageStored.AsyncTestListener("manager", 1) - // Attempt to deliver a message to generate event. + // Deliver a message to trigger event. origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com") recip, _ := sm.AddrPolicy.NewRecipient("to@example.com") if err := sm.Deliver( @@ -75,6 +201,46 @@ func TestDeliverEmitsAfterMessageStoredEvent(t *testing.T) { assertMessageCount(t, sm, "to@example.com", 1) } +func TestDeliverBeforeAndAfterMessageStoredEvents(t *testing.T) { + sm, extHost := testStoreManager() + + // Register function to receive Before event. + extHost.Events.BeforeMessageStored.AddListener( + "test", + func(msg event.InboundMessage) *event.InboundMessage { + // Listener rewrites destination mailboxes. + resp := msg + resp.Mailboxes = []string{"new1@example.com", "new2@example.com"} + return &resp + }) + + // After event listener. + listener := extHost.Events.AfterMessageStored.AsyncTestListener("manager", 2) + + // Deliver a message to trigger events. + origin, _ := sm.AddrPolicy.ParseOrigin("from@example.com") + recip1, _ := sm.AddrPolicy.NewRecipient("u1@example.com") + recip2, _ := sm.AddrPolicy.NewRecipient("u2@example.com") + if err := sm.Deliver( + origin, + []*policy.Recipient{recip1, recip2}, + "Received: xyz\r\n", + []byte("From: from@example.com\nSubject: tsub\n\ntest email"), + ); err != nil { + t.Fatal(err) + } + + // Confirm mailbox names overriden by Before were sent to After event. Order is + // not guaranteed. + got1, err := listener() + require.NoError(t, err) + got2, err := listener() + require.NoError(t, err) + got := []string{got1.Mailbox, got2.Mailbox} + assert.Contains(t, got, "new1@example.com") + assert.Contains(t, got, "new2@example.com") +} + // Returns an empty StoreManager and extension Host pair, configured for testing. func testStoreManager() (*message.StoreManager, *extension.Host) { extHost := extension.NewHost() diff --git a/pkg/server/smtp/handler_test.go b/pkg/server/smtp/handler_test.go index fd342f6..b5e9305 100644 --- a/pkg/server/smtp/handler_test.go +++ b/pkg/server/smtp/handler_test.go @@ -589,7 +589,7 @@ func setupSMTPServer(ds storage.Store, extHost *extension.Host) *Server { // Create a server, don't start it. addrPolicy := &policy.Addressing{Config: cfg} - manager := &message.StoreManager{Store: ds} + manager := &message.StoreManager{Store: ds, ExtHost: extHost} return NewServer(cfg.SMTP, manager, addrPolicy, extHost) } diff --git a/pkg/test/testdata/basic.golden b/pkg/test/testdata/basic.golden index 014c93f..31035dc 100644 --- a/pkg/test/testdata/basic.golden +++ b/pkg/test/testdata/basic.golden @@ -2,7 +2,7 @@ Mailbox: recipient From: To: [] Subject: basic subject -Size: 217 +Size: 204 BODY TEXT: Basic message. diff --git a/pkg/test/testdata/encodedheader.golden b/pkg/test/testdata/encodedheader.golden index af11c2a..cd2d5d8 100644 --- a/pkg/test/testdata/encodedheader.golden +++ b/pkg/test/testdata/encodedheader.golden @@ -2,7 +2,7 @@ Mailbox: recipient From: X-äéß Y-äéß To: [Test of ȇɲʢȯȡɪɴʛ ] Subject: Test of ȇɲʢȯȡɪɴʛ -Size: 351 +Size: 338 BODY TEXT: Basic message. diff --git a/pkg/test/testdata/fullname.golden b/pkg/test/testdata/fullname.golden index b40a967..31bb3d2 100644 --- a/pkg/test/testdata/fullname.golden +++ b/pkg/test/testdata/fullname.golden @@ -2,7 +2,7 @@ Mailbox: recipient From: From User To: [Rec I. Pient ] Subject: basic subject -Size: 246 +Size: 233 BODY TEXT: Basic message. diff --git a/pkg/test/testdata/no-to-ipv4.golden b/pkg/test/testdata/no-to-ipv4.golden index c9342a9..af7a9e5 100644 --- a/pkg/test/testdata/no-to-ipv4.golden +++ b/pkg/test/testdata/no-to-ipv4.golden @@ -2,7 +2,7 @@ Mailbox: ip4recipient From: To: [] Subject: basic subject -Size: 198 +Size: 180 BODY TEXT: No-To message. diff --git a/pkg/test/testdata/no-to-ipv6.golden b/pkg/test/testdata/no-to-ipv6.golden index 8846de4..38195c5 100644 --- a/pkg/test/testdata/no-to-ipv6.golden +++ b/pkg/test/testdata/no-to-ipv6.golden @@ -2,7 +2,7 @@ Mailbox: ip6recipient From: To: [] Subject: basic subject -Size: 227 +Size: 180 BODY TEXT: No-To message.