diff --git a/.gitignore b/.gitignore index a02c7d1..00a88f1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ go.work.sum # Dist dist/ + +# smtp2shoutrrr +config.toml diff --git a/.goreleaser.yml b/.goreleaser.yml index cf07385..1d3c91c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -68,29 +68,39 @@ dockers: build_flag_templates: - "--pull" - "--platform=linux/arm64" -# - image_templates: -# - &armv7_image "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}-armv7" -# use: buildx -# dockerfile: *dockerfile -# goos: linux -# goarch: arm -# goarm: "7" -# build_flag_templates: -# - "--pull" -# - "--platform=linux/arm/v7" +- image_templates: + - &armv7_image "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}-armv7" + use: buildx + dockerfile: *dockerfile + goos: linux + goarch: arm + goarm: "7" + build_flag_templates: + - "--pull" + - "--platform=linux/arm/v7" docker_manifests: - name_template: "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}" image_templates: - *amd64_image - *arm64_image - # - *armv7_image + - *armv7_image # - name_template: "git.nakama.town/fmartingr/smtp2shoutrrr:latest" # image_templates: # - *amd64_image # - *arm64_image # - *armv7_image +nfpms: + - maintainer: Felipe Martin + description: SMTP server to forward messages to shoutrrr endpoints + homepage: https://git.nakama.town/fmartingr/smtp2shoutrrr + license: AGPL-3.0 + formats: + - deb + - rpm + - apk + upx: - enabled: true ids: diff --git a/README.md b/README.md index 72e6968..1b62f59 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,64 @@ A simple SMTP server that forwards incoming emails to a Shoutrrr service. +## Installing + +First generate a new configuration file following this example: + +```toml +# config.toml +# Port the SMTP server will listen on +Port = 11025 + +# Credentials for the SMTP server (default to username/password if not set/empty) +Username = "user" +Password = "nometokens" + +# Shoutrrr service to forward emails to +[[Recipients]] +# Email addresses to forward emails from +Addresses = ["some@email.com"] +# Shoutrrr service to forward emails to +Target = "ntfy://ntfy.sh/my-ntfy-topic?tags=thing" + +# Note: Repeat [[Recipients]] as needed +``` + +### From releases + +- Grab the latest release from the [releases page](https://git.nakama.town/fmartingr/smtp2shoutrrr/releases) +- Put the configuration file in the same directory as the binary +- Run the binary for your appropriate platform + +### From source (development) + +- Clone [this repository](https://git.nakama.town/fmartingr/smtp2shoutrrr) +- Put the configuration file in the repository folder +- Run `make quick-run` + +### Using docker + +- Create a `config.toml` file as described above +- Run the docker image mounting the `Config.toml` file as `/config.toml` and exposing the configured port: + +```bash +docker run -v /path/to/config.toml:/config.toml \ + -p 11025:11025 \ + git.nakama.town/fmartingr/smtp2shoutrrr:latest +``` + ## Development Run the server with: ``` -go run main.go +make quick-run ``` Send a test email with: +> This will read the `config.toml` in the current directory to set the appropriate SMTP client configuration. + +``` +go run ./cmd/sendmail/. ``` -go run ./cmd/sendmail/main.go diff --git a/cmd/sendmail/main.go b/cmd/sendmail/main.go index 155481e..df894b3 100644 --- a/cmd/sendmail/main.go +++ b/cmd/sendmail/main.go @@ -1,11 +1,16 @@ package main import ( + "fmt" "log" "log/slog" "net/smtp" + "os" + "git.nakama.town/fmartingr/gotoolkit/encoding" "github.com/emersion/go-sasl" + + "git.nakama.town/fmartingr/smtp2shoutrrr" ) // The ANONYMOUS mechanism name. @@ -47,7 +52,7 @@ func (a *plainClient) Start(si *smtp.ServerInfo) (mech string, ir []byte, err er } func (a *plainClient) Next(challenge []byte, b bool) (response []byte, err error) { - slog.Info("Next: %v", challenge) + slog.Info("Next: %v", slog.String("challenge", string(challenge))) return nil, nil } @@ -59,14 +64,44 @@ func NewPlainClient(identity, username, password string) smtp.Auth { } func main() { + var config smtp2shoutrrr.Config + configPath := "config.toml" + + f, err := os.Open(configPath) + if err != nil { + slog.Error("Error opening config file", slog.String("err", err.Error()), slog.String("path", configPath)) + return + } + + enc := encoding.NewTOMLEncoding() + if err := enc.DecodeReader(f, &config); err != nil { + slog.Error("Error decoding config file", slog.String("err", err.Error()), slog.String("path", configPath)) + return + } + + config.SetDefaults() + // hostname is used by PlainAuth to validate the TLS certificate. hostname := "localhost" - auth := NewPlainClient("", "username", "password") + auth := NewPlainClient("", config.Username, config.Password) // auth := NewAnonymousClient("test") - recipients := []string{"something@something.com", "caca@caca.com"} - msg := []byte("\r\nwithout title") + + slog.Info("Using first recipient configuration to send a test email") + + if len(config.Recipients) == 0 { + slog.Error("No recipients found in configuration") + return + } + + if len(config.Recipients[0].Addresses) == 0 { + slog.Error("No email addresses found in first recipient configuration") + return + } + + recipients := []string{config.Recipients[0].Addresses[0]} + msg := []byte("Subject: Test notification\r\n\r\nThis is a test notification") from := "hello@localhost" - err := smtp.SendMail(hostname+":11025", auth, from, recipients, msg) + err = smtp.SendMail(fmt.Sprintf("%s:%d", hostname, config.Port), auth, from, recipients, msg) if err != nil { log.Fatal(err) } diff --git a/cmd/smtp2shoutrrr/main.go b/cmd/smtp2shoutrrr/main.go index 41bc0d2..ef2c937 100644 --- a/cmd/smtp2shoutrrr/main.go +++ b/cmd/smtp2shoutrrr/main.go @@ -3,7 +3,6 @@ package main import ( "bytes" "context" - "errors" "fmt" "io" "log" @@ -23,6 +22,8 @@ import ( "github.com/containrrr/shoutrrr" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" + + "git.nakama.town/fmartingr/smtp2shoutrrr" ) type ReceivedEmail struct { @@ -82,39 +83,11 @@ func (re *ReceivedEmail) Body() (string, error) { return re.body, nil } -type Config struct { - Port int - Recipients []ConfigRecipient -} - -func (c *Config) SetDefaults() { - if c.Port == 0 { - c.Port = 11125 - } -} - -type ConfigRecipient struct { - Addresses []string // email addresses - Target string // shoutrrr address - targetURL *url.URL -} - -func (cr ConfigRecipient) GetTargetURL() *url.URL { - if cr.targetURL == nil { - var err error - cr.targetURL, err = url.Parse(cr.Target) - if err != nil { - slog.Error("failed to parse shoutrrr target URL", slog.String("target", cr.Target), slog.String("err", err.Error())) - } - } - return cr.targetURL -} - var _ model.Server = (*smtpServer)(nil) type smtpServer struct { backend *smtp.Server - config Config + config smtp2shoutrrr.Config } func (s *smtpServer) IsEnabled() bool { @@ -131,7 +104,7 @@ func (s *smtpServer) Stop(ctx context.Context) error { return s.backend.Shutdown(ctx) } -func NewSMTPServer(config *Config) model.Server { +func NewSMTPServer(config *smtp2shoutrrr.Config) model.Server { be := &Backend{ config: config, } @@ -152,13 +125,14 @@ func NewSMTPServer(config *Config) model.Server { // The Backend implements SMTP server methods. type Backend struct { - config *Config + config *smtp2shoutrrr.Config } // NewSession is called after client greeting (EHLO, HELO). func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) { return &Session{ forwarderFunc: bkd.forwardEmail, + config: bkd.config, }, nil } @@ -200,6 +174,8 @@ type Session struct { addresses []string body string + config *smtp2shoutrrr.Config + forwarderFunc func(ReceivedEmail) error } @@ -212,8 +188,8 @@ func (s *Session) AuthMechanisms() []string { // Auth is the handler for supported authenticators. func (s *Session) Auth(mech string) (sasl.Server, error) { return sasl.NewPlainServer(func(identity, username, password string) error { - if username != "username" || password != "password" { - return errors.New("Invalid username or password") + if username != s.config.Username && password != s.config.Password { + return fmt.Errorf("invalid credentials") } return nil }), nil @@ -256,7 +232,7 @@ func (s *Session) Logout() error { } func main() { - var config *Config + var config *smtp2shoutrrr.Config configPath := "config.toml" f, err := os.Open(configPath) diff --git a/config.go b/config.go new file mode 100644 index 0000000..9a64e9f --- /dev/null +++ b/config.go @@ -0,0 +1,46 @@ +package smtp2shoutrrr + +import ( + "log/slog" + "net/url" +) + +type Config struct { + Port int + Username string + Password string + Recipients []ConfigRecipient +} + +func (c *Config) SetDefaults() { + if c.Port == 0 { + c.Port = 11125 + } + + if c.Username == "" { + slog.Warn("no username provided, using default: username") + c.Username = "username" + } + + if c.Password == "" { + slog.Warn("no password provided, using default: password") + c.Password = "password" + } +} + +type ConfigRecipient struct { + Addresses []string // email addresses + Target string // shoutrrr address + targetURL *url.URL +} + +func (cr ConfigRecipient) GetTargetURL() *url.URL { + if cr.targetURL == nil { + var err error + cr.targetURL, err = url.Parse(cr.Target) + if err != nil { + slog.Error("failed to parse shoutrrr target URL", slog.String("target", cr.Target), slog.String("err", err.Error())) + } + } + return cr.targetURL +} diff --git a/config.toml b/config.toml deleted file mode 100644 index 362f514..0000000 --- a/config.toml +++ /dev/null @@ -1,9 +0,0 @@ -Port = 11025 - -[[Recipients]] -Addresses = ["something@something.com"] -Target = "ntfy://ntfy.sh/fmartingr-dev" - -[[Recipients]] -Addresses = ["caca@caca.com"] -Target = "ntfy://ntfy.sh/fmartingr-dev"