From 62344993f55fd01b780aadc6822f138cdffff54b Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 5 Nov 2020 19:48:18 +0100 Subject: [PATCH] Allow email relaying to a real MTA --- cmd/fake-smtp/container.go | 5 + cmd/fake-smtp/smtp.go | 22 ++++- go.mod | 2 +- go.sum | 4 +- internal/command/relay_email.go | 164 ++++++++++++++++++++++++++++++++ internal/config/config.go | 22 ++++- misc/script/test-smtp.sh | 2 +- 7 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 internal/command/relay_email.go diff --git a/cmd/fake-smtp/container.go b/cmd/fake-smtp/container.go index 99672c9..6cca16e 100644 --- a/cmd/fake-smtp/container.go +++ b/cmd/fake-smtp/container.go @@ -53,6 +53,11 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) { cqrs.CommandHandlerFunc(command.HandleDeleteEmail), ) + bus.RegisterCommand( + cqrs.MatchCommandRequest(&command.RelayEmailRequest{}), + cqrs.CommandHandlerFunc(command.HandleRelayEmail), + ) + bus.RegisterQuery( cqrs.MatchQueryRequest(&query.GetInboxRequest{}), cqrs.QueryHandlerFunc(query.HandleGetInbox), diff --git a/cmd/fake-smtp/smtp.go b/cmd/fake-smtp/smtp.go index 509dacc..e299a81 100644 --- a/cmd/fake-smtp/smtp.go +++ b/cmd/fake-smtp/smtp.go @@ -12,6 +12,7 @@ import ( "github.com/jhillyerd/enmime" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/cqrs" + "gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/middleware/container" "gitlab.com/wpetit/goweb/service" ) @@ -74,12 +75,31 @@ func (s *Session) Data(r io.Reader) error { return errors.Wrap(err, "could not retrieve cqrs service") } + conf, err := config.From(s.ctn) + if err != nil { + return errors.Wrap(err, "could not retrieve config service") + } + + if conf.Relay.Enabled { + cmd := &command.RelayEmailRequest{ + Envelope: env, + } + + if _, err := bus.Exec(s.ctx, cmd); err != nil { + logger.Error(s.ctx, "could not exec command", logger.E(err)) + + return errors.Wrapf(err, "could not exec '%T' command", cmd) + } + } + cmd := &command.StoreEmailRequest{ Envelope: env, } if _, err := bus.Exec(s.ctx, cmd); err != nil { - return errors.Wrap(err, "could not exec command") + logger.Error(s.ctx, "could not exec command", logger.E(err)) + + return errors.Wrapf(err, "could not exec '%T' command", cmd) } return nil diff --git a/go.mod b/go.mod index a29dc18..2aa2df2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/asdine/storm/v3 v3.1.1 github.com/caarlos0/env/v6 v6.2.1 - github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b // indirect + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.12.1 github.com/go-chi/chi v4.1.1+incompatible github.com/jhillyerd/enmime v0.8.0 diff --git a/go.sum b/go.sum index e927054..8e1111c 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= -github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= -github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5sNA= github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/internal/command/relay_email.go b/internal/command/relay_email.go new file mode 100644 index 0000000..471cc24 --- /dev/null +++ b/internal/command/relay_email.go @@ -0,0 +1,164 @@ +package command + +import ( + "context" + "crypto/tls" + "io" + "sync" + + "github.com/emersion/go-sasl" + + "github.com/emersion/go-smtp" + + "forge.cadoles.com/wpetit/fake-smtp/internal/config" + "github.com/jhillyerd/enmime" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/cqrs" + "gitlab.com/wpetit/goweb/middleware/container" +) + +type RelayEmailRequest struct { + Envelope *enmime.Envelope +} + +func HandleRelayEmail(ctx context.Context, cmd cqrs.Command) error { + req, ok := cmd.Request().(*RelayEmailRequest) + if !ok { + return cqrs.ErrUnexpectedRequest + } + + ctn, err := container.From(ctx) + if err != nil { + return errors.Wrap(err, "could not retrieve service container") + } + + conf, err := config.From(ctn) + if err != nil { + return errors.Wrap(err, "could not retrieve config service") + } + + relay := conf.Relay + + if err := forwardMail(req.Envelope, relay); err != nil { + return errors.Wrap(err, "could not forward mail") + } + + return nil +} + +func forwardMail(env *enmime.Envelope, conf config.RelayConfig) error { + var tlsConfig *tls.Config + + if conf.InsecureSkipVerify { + tlsConfig = &tls.Config{ + // nolint: gosec + InsecureSkipVerify: true, + } + } + + addr := conf.Address + + var ( + client *smtp.Client + err error + ) + + if conf.UseTLS { + client, err = smtp.DialTLS(addr, tlsConfig) + if err != nil { + return errors.WithStack(err) + } + } else { + client, err = smtp.Dial(addr) + if err != nil { + return errors.WithStack(err) + } + } + defer client.Close() + + if err = client.Hello("localhost"); err != nil { + return errors.WithStack(err) + } + + if ok, _ := client.Extension("STARTTLS"); ok { + if err = client.StartTLS(tlsConfig); err != nil { + return errors.WithStack(err) + } + } + + if conf.Username != "" || conf.Password != "" { + if ok, _ := client.Extension("AUTH"); ok { + var auth sasl.Client + if conf.Anonymous { + auth = sasl.NewAnonymousClient("fakesmtp") + } else { + auth = sasl.NewPlainClient(conf.Identity, conf.Username, conf.Password) + } + + if err := client.Auth(auth); err != nil { + return errors.WithStack(err) + } + } + } + + var from string + + if conf.FromOverride != "" { + from = conf.FromOverride + } else { + from = env.GetHeader("From") + } + + if err = client.Mail(from, nil); err != nil { + return errors.WithStack(err) + } + + to := env.GetHeaderValues("To") + + for _, addr := range to { + if err = client.Rcpt(addr); err != nil { + return errors.WithStack(err) + } + } + + w, err := client.Data() + if err != nil { + return errors.WithStack(err) + } + + pr, pw := io.Pipe() + + var wg sync.WaitGroup + + wg.Add(1) + + go func() { + defer wg.Done() + defer pw.Close() + + if err = env.Root.Encode(pw); err != nil { + err = errors.WithStack(err) + } + }() + + _, err = io.Copy(w, pr) + if err != nil { + return err + } + + wg.Wait() + + if err != nil { + return errors.WithStack(err) + } + + if err = w.Close(); err != nil { + return err + } + + if err := client.Quit(); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index cda8dd7..45c1b55 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,9 +12,10 @@ import ( ) type Config struct { - HTTP HTTPConfig `yaml:"http"` - SMTP SMTPConfig `yaml:"smtp"` - Data DataConfig `ymal:"data"` + HTTP HTTPConfig `yaml:"http"` + SMTP SMTPConfig `yaml:"smtp"` + Data DataConfig `yaml:"data"` + Relay RelayConfig `yaml:"relay"` } type HTTPConfig struct { @@ -36,6 +37,18 @@ type SMTPConfig struct { Debug bool `yaml:"debug" env:"FAKESMTP_SMTP_DEBUG"` } +type RelayConfig struct { + Enabled bool `yaml:"enabled" env:"FAKESMTP_RELAY_ENABLED"` + Address string `yaml:"address" env:"FAKESMTP_RELAY_ADDRESS"` + Identity string `yaml:"identity" env:"FAKESMTP_RELAY_IDENTITY"` + Username string `yaml:"username" env:"FAKESMTP_RELAY_USERNAME"` + Password string `yaml:"password" env:"FAKESMTP_RELAY_PASSWORD"` + Anonymous bool `yaml:"anonymous" env:"FAKESMTP_RELAY_ANONYMOUS"` + UseTLS bool `yaml:"useTLS" env:"FAKESMTP_RELAY_USE_TLS"` + InsecureSkipVerify bool `yaml:"insecureSkipVerify" env:"FAKESMTP_RELAY_INSECURE_SKIP_VERIFY"` + FromOverride string `yaml:"fromOverride" env:"FAKESMTP_RELAY_FROM_OVERRIDE"` +} + type DataConfig struct { Path string `yaml:"path" env:"FAKESMTP_DATA_PATH"` } @@ -91,6 +104,9 @@ func NewDefault() *Config { Data: DataConfig{ Path: "fakesmtp.db", }, + Relay: RelayConfig{ + Enabled: false, + }, } } diff --git a/misc/script/test-smtp.sh b/misc/script/test-smtp.sh index 5167661..16a7fe4 100755 --- a/misc/script/test-smtp.sh +++ b/misc/script/test-smtp.sh @@ -6,7 +6,7 @@ for i in {1..10}; do -s "Test ${i}" \ -a README.md \ -a package.json \ - foo_${i}@bar_${i}.com \ + foo_${i}@bar${i}.com \ <