Compare commits
No commits in common. "0ec1c15e8ec4cd4ad0c9c4855e00166b5e166f80" and "77dd7af4acfe47077c39ad08a9425329fd3d9f40" have entirely different histories.
0ec1c15e8e
...
77dd7af4ac
7 changed files with 62 additions and 172 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -28,6 +28,3 @@ go.work.sum
|
||||||
|
|
||||||
# Dist
|
# Dist
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# smtp2shoutrrr
|
|
||||||
config.toml
|
|
||||||
|
|
|
@ -68,39 +68,29 @@ dockers:
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--pull"
|
- "--pull"
|
||||||
- "--platform=linux/arm64"
|
- "--platform=linux/arm64"
|
||||||
- image_templates:
|
# - image_templates:
|
||||||
- &armv7_image "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}-armv7"
|
# - &armv7_image "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}-armv7"
|
||||||
use: buildx
|
# use: buildx
|
||||||
dockerfile: *dockerfile
|
# dockerfile: *dockerfile
|
||||||
goos: linux
|
# goos: linux
|
||||||
goarch: arm
|
# goarch: arm
|
||||||
goarm: "7"
|
# goarm: "7"
|
||||||
build_flag_templates:
|
# build_flag_templates:
|
||||||
- "--pull"
|
# - "--pull"
|
||||||
- "--platform=linux/arm/v7"
|
# - "--platform=linux/arm/v7"
|
||||||
|
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}"
|
- name_template: "git.nakama.town/fmartingr/smtp2shoutrrr:{{ .Version }}"
|
||||||
image_templates:
|
image_templates:
|
||||||
- *amd64_image
|
- *amd64_image
|
||||||
- *arm64_image
|
- *arm64_image
|
||||||
- *armv7_image
|
# - *armv7_image
|
||||||
# - name_template: "git.nakama.town/fmartingr/smtp2shoutrrr:latest"
|
# - name_template: "git.nakama.town/fmartingr/smtp2shoutrrr:latest"
|
||||||
# image_templates:
|
# image_templates:
|
||||||
# - *amd64_image
|
# - *amd64_image
|
||||||
# - *arm64_image
|
# - *arm64_image
|
||||||
# - *armv7_image
|
# - *armv7_image
|
||||||
|
|
||||||
nfpms:
|
|
||||||
- maintainer: Felipe Martin <me@fmartingr.com>
|
|
||||||
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:
|
upx:
|
||||||
- enabled: true
|
- enabled: true
|
||||||
ids:
|
ids:
|
||||||
|
|
53
README.md
53
README.md
|
@ -2,64 +2,15 @@
|
||||||
|
|
||||||
A simple SMTP server that forwards incoming emails to a Shoutrrr service.
|
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
|
## Development
|
||||||
|
|
||||||
Run the server with:
|
Run the server with:
|
||||||
|
|
||||||
```
|
```
|
||||||
make quick-run
|
go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
Send a test email with:
|
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
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.nakama.town/fmartingr/gotoolkit/encoding"
|
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
|
|
||||||
"git.nakama.town/fmartingr/smtp2shoutrrr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The ANONYMOUS mechanism name.
|
// The ANONYMOUS mechanism name.
|
||||||
|
@ -52,7 +47,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) {
|
func (a *plainClient) Next(challenge []byte, b bool) (response []byte, err error) {
|
||||||
slog.Info("Next: %v", slog.String("challenge", string(challenge)))
|
slog.Info("Next: %v", challenge)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,44 +59,14 @@ func NewPlainClient(identity, username, password string) smtp.Auth {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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 is used by PlainAuth to validate the TLS certificate.
|
||||||
hostname := "localhost"
|
hostname := "localhost"
|
||||||
auth := NewPlainClient("", config.Username, config.Password)
|
auth := NewPlainClient("", "username", "password")
|
||||||
// auth := NewAnonymousClient("test")
|
// auth := NewAnonymousClient("test")
|
||||||
|
recipients := []string{"something@something.com", "caca@caca.com"}
|
||||||
slog.Info("Using first recipient configuration to send a test email")
|
msg := []byte("\r\nwithout title")
|
||||||
|
|
||||||
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"
|
from := "hello@localhost"
|
||||||
err = smtp.SendMail(fmt.Sprintf("%s:%d", hostname, config.Port), auth, from, recipients, msg)
|
err := smtp.SendMail(hostname+":11025", auth, from, recipients, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -22,8 +23,6 @@ import (
|
||||||
"github.com/containrrr/shoutrrr"
|
"github.com/containrrr/shoutrrr"
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
|
|
||||||
"git.nakama.town/fmartingr/smtp2shoutrrr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReceivedEmail struct {
|
type ReceivedEmail struct {
|
||||||
|
@ -83,11 +82,39 @@ func (re *ReceivedEmail) Body() (string, error) {
|
||||||
return re.body, nil
|
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)
|
var _ model.Server = (*smtpServer)(nil)
|
||||||
|
|
||||||
type smtpServer struct {
|
type smtpServer struct {
|
||||||
backend *smtp.Server
|
backend *smtp.Server
|
||||||
config smtp2shoutrrr.Config
|
config Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpServer) IsEnabled() bool {
|
func (s *smtpServer) IsEnabled() bool {
|
||||||
|
@ -104,7 +131,7 @@ func (s *smtpServer) Stop(ctx context.Context) error {
|
||||||
return s.backend.Shutdown(ctx)
|
return s.backend.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSMTPServer(config *smtp2shoutrrr.Config) model.Server {
|
func NewSMTPServer(config *Config) model.Server {
|
||||||
be := &Backend{
|
be := &Backend{
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
@ -125,14 +152,13 @@ func NewSMTPServer(config *smtp2shoutrrr.Config) model.Server {
|
||||||
|
|
||||||
// The Backend implements SMTP server methods.
|
// The Backend implements SMTP server methods.
|
||||||
type Backend struct {
|
type Backend struct {
|
||||||
config *smtp2shoutrrr.Config
|
config *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSession is called after client greeting (EHLO, HELO).
|
// NewSession is called after client greeting (EHLO, HELO).
|
||||||
func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
|
func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
|
||||||
return &Session{
|
return &Session{
|
||||||
forwarderFunc: bkd.forwardEmail,
|
forwarderFunc: bkd.forwardEmail,
|
||||||
config: bkd.config,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,8 +200,6 @@ type Session struct {
|
||||||
addresses []string
|
addresses []string
|
||||||
body string
|
body string
|
||||||
|
|
||||||
config *smtp2shoutrrr.Config
|
|
||||||
|
|
||||||
forwarderFunc func(ReceivedEmail) error
|
forwarderFunc func(ReceivedEmail) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,8 +212,8 @@ func (s *Session) AuthMechanisms() []string {
|
||||||
// Auth is the handler for supported authenticators.
|
// Auth is the handler for supported authenticators.
|
||||||
func (s *Session) Auth(mech string) (sasl.Server, error) {
|
func (s *Session) Auth(mech string) (sasl.Server, error) {
|
||||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||||
if username != s.config.Username && password != s.config.Password {
|
if username != "username" || password != "password" {
|
||||||
return fmt.Errorf("invalid credentials")
|
return errors.New("Invalid username or password")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}), nil
|
}), nil
|
||||||
|
@ -232,7 +256,7 @@ func (s *Session) Logout() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var config *smtp2shoutrrr.Config
|
var config *Config
|
||||||
configPath := "config.toml"
|
configPath := "config.toml"
|
||||||
|
|
||||||
f, err := os.Open(configPath)
|
f, err := os.Open(configPath)
|
||||||
|
|
46
config.go
46
config.go
|
@ -1,46 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
9
config.toml
Normal file
9
config.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Port = 11025
|
||||||
|
|
||||||
|
[[Recipients]]
|
||||||
|
Addresses = ["something@something.com"]
|
||||||
|
Target = "ntfy://ntfy.sh/fmartingr-dev"
|
||||||
|
|
||||||
|
[[Recipients]]
|
||||||
|
Addresses = ["caca@caca.com"]
|
||||||
|
Target = "ntfy://ntfy.sh/fmartingr-dev"
|
Loading…
Add table
Add a link
Reference in a new issue