Skip to content

Commit

Permalink
SMTP: Fixes, minimum implementation for testmode, test stub for testmode
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Hinderberger committed Jun 21, 2024
1 parent 21e67d0 commit 2171791
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 21 deletions.
5 changes: 3 additions & 2 deletions api_testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/sirupsen/logrus"

"github.com/programmfabrik/apitest/internal/httpproxy"
"github.com/programmfabrik/apitest/internal/smtp"
"github.com/programmfabrik/apitest/pkg/lib/datastore"
"github.com/programmfabrik/apitest/pkg/lib/filesystem"
"github.com/programmfabrik/apitest/pkg/lib/report"
Expand All @@ -36,7 +37,7 @@ type Suite struct {
} `json:"http_server,omitempty"`
SmtpServer *struct {
Addr string `json:"addr"`
}
} `json:"smtp_server,omitempty"`
Tests []any `json:"tests"`
Store map[string]any `json:"store"`

Expand All @@ -57,7 +58,7 @@ type Suite struct {
idleConnsClosed chan struct{}
HTTPServerHost string
loader template.Loader
smtpServer *SmtpServer
smtpServer *smtp.Server
}

// NewTestSuite creates a new suite on which we execute our tests on. Normally this only gets call from within the apitest main command
Expand Down
15 changes: 14 additions & 1 deletion internal/smtp/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ type ReceivedMessage struct {
receivedAt time.Time
}

// TODO: Constructor that takes in from, rcptTo, rawMessageData, receivedAt and that also parses the message
func NewReceivedMessage(
from string, rcptTo []string, rawMessageData []byte, receivedAt time.Time,
) (*ReceivedMessage, error) {
msg := &ReceivedMessage{
smtpFrom: from,
smtpRcptTo: rcptTo,
rawMessageData: rawMessageData,
receivedAt: receivedAt,
}

// TODO: Parse message

return msg, nil
}

// TODO: Getters
99 changes: 82 additions & 17 deletions internal/smtp/server.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package smtp

import (
"context"
"fmt"
"io"
"regexp"
"sync"
"time"

"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)

// Server contains a basic SMTP server for testing purposes.
Expand All @@ -14,27 +18,33 @@ import (
// received messages, which can be queried using the appropriate methods
// of Server.
type Server struct {
server smtp.Server
server *smtp.Server
receivedMessages []*ReceivedMessage

mutex sync.RWMutex
}

type session struct {
// TODO
server *Server
conn *smtp.Conn

from string
rcptTo []string
}

func NewServer(addr string) *Server {
server := &Server{
receivedMessages: make([]receivedMessage),
}
server := new(Server)

backend := panic("not implemented") // TODO
backend := smtp.BackendFunc(func(c *smtp.Conn) (smtp.Session, error) {
return newSession(server, c)
})

server.server = smtp.NewServer(backend)
server.Addr = addr
// TODO: Enable SMTPUTF8?
// TODO: Enable BINARYMIME?
s := smtp.NewServer(backend)
s.Addr = addr
s.EnableSMTPUTF8 = true
s.EnableBINARYMIME = true

server.server = s

return server
}
Expand All @@ -47,8 +57,8 @@ func (s *Server) ListenAndServe() error {

// Shutdown shuts down the SMTP server that was previously started using
// ListenAndServe.
func (s *Server) Shutdown() error {
return s.server.Shutdown()
func (s *Server) Shutdown(ctx context.Context) error {
return s.server.Shutdown(ctx)
}

// ReceivedMessage returns a message that the server has retrieved
Expand All @@ -57,25 +67,24 @@ func (s *Server) ReceivedMessage(idx int) (*ReceivedMessage, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()

msg, ok := s.receivedMessages[idx]
if !ok {
if idx >= len(s.receivedMessages) {
return nil, fmt.Errorf(
"Server does not contain message with index %d", idx,
)
}

return msg, nil
return s.receivedMessages[idx], nil
}

// ReceivedMessages returns the list of all messages that the server has
// retrieved.
func (s *Server) ReceivedMessages() []ReceivedMessage {
func (s *Server) ReceivedMessages() []*ReceivedMessage {
s.mutex.RLock()
defer s.mutex.RUnlock()

// We copy the slice to avoid race conditions when the receivedMessages slice is updated.
// It's just a slice of pointers, so it should be relatively lightweight.
view := make([]*ReceivedMessage, len(s.receivedMessage))
view := make([]*ReceivedMessage, len(s.receivedMessages))
copy(view, s.receivedMessages)

return view
Expand All @@ -90,3 +99,59 @@ func (s *Server) SearchByHeader(re *regexp.Regexp) []ReceivedMessage {
// TODO
panic("not implemented")
}

func newSession(server *Server, c *smtp.Conn) (smtp.Session, error) {
return &session{
server: server,
conn: c,
}, nil
}

// Implements smtp.Session's Data method.
func (s *session) Data(r io.Reader) error {
rawData, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("could not read mail data from SMTP: %w", err)
}

s.server.mutex.Lock()
defer s.server.mutex.Unlock()

now := time.Now()

logrus.Infof("SMTP: Received message from %s to %v at %v", s.from, s.rcptTo, now)
msg, err := NewReceivedMessage(s.from, s.rcptTo, rawData, now)
if err != nil {
return fmt.Errorf("error constructing ReceivedMessage in SMTP server: %w", err)
}

s.server.receivedMessages = append(s.server.receivedMessages, msg)

return nil
}

// Implements smtp.Session's Logout method.
func (s *session) Logout() error {
s.Reset()
return nil
}

// Implements smtp.Session's Mail method.
func (s *session) Mail(from string, opts *smtp.MailOptions) error {
// opts are currently ignored
s.from = from
return nil
}

// Implements smtp.Session's Rcpt method.
func (s *session) Rcpt(to string, opts *smtp.RcptOptions) error {
// opts are currently ignored
s.rcptTo = append(s.rcptTo, to)
return nil
}

// Implements smtp.Session's Reset method.
func (s *session) Reset() {
s.from = ""
s.rcptTo = nil
}
5 changes: 4 additions & 1 deletion smtp_server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"

esmtp "github.com/emersion/go-smtp"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -35,7 +37,8 @@ func (ats *Suite) StopSmtpServer() {
return
}

err := ats.smtpServer.Shutdown()
// TODO: Shouldn't this use a context with a timeout (also at http_server.go)?
err := ats.smtpServer.Shutdown(context.Background())
if err != nil {
// logrus.Error is used instead of Fatal, because an error
// during closing of a server shouldn't affect the outcome of
Expand Down
10 changes: 10 additions & 0 deletions test/email/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"http_server": {
"addr": ":9999",
"testmode": true
},
"smtp_server": {
"addr": ":9925"
},
"name": "smtp server test mode"
}

0 comments on commit 2171791

Please sign in to comment.