Compare commits
8 commits
77dd7af4ac
...
0ec1c15e8e
Author | SHA1 | Date | |
---|---|---|---|
0ec1c15e8e | |||
b8292cc5ba | |||
34c1c5a57c | |||
f96a7a5d7c | |||
c829408095 | |||
f1733d3670 | |||
97fc425c79 | |||
d42c97ac62 |
7 changed files with 172 additions and 62 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -28,3 +28,6 @@ go.work.sum
|
||||||
|
|
||||||
# Dist
|
# Dist
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# smtp2shoutrrr
|
||||||
|
config.toml
|
||||||
|
|
|
@ -68,29 +68,39 @@ 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,15 +2,64 @@
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
```
|
```
|
||||||
go run main.go
|
make quick-run
|
||||||
```
|
```
|
||||||
|
|
||||||
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,11 +1,16 @@
|
||||||
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.
|
||||||
|
@ -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) {
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,14 +64,44 @@ 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("", "username", "password")
|
auth := NewPlainClient("", config.Username, config.Password)
|
||||||
// auth := NewAnonymousClient("test")
|
// 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"
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -23,6 +22,8 @@ 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 {
|
||||||
|
@ -82,39 +83,11 @@ 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 Config
|
config smtp2shoutrrr.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpServer) IsEnabled() bool {
|
func (s *smtpServer) IsEnabled() bool {
|
||||||
|
@ -131,7 +104,7 @@ func (s *smtpServer) Stop(ctx context.Context) error {
|
||||||
return s.backend.Shutdown(ctx)
|
return s.backend.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSMTPServer(config *Config) model.Server {
|
func NewSMTPServer(config *smtp2shoutrrr.Config) model.Server {
|
||||||
be := &Backend{
|
be := &Backend{
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
@ -152,13 +125,14 @@ func NewSMTPServer(config *Config) model.Server {
|
||||||
|
|
||||||
// The Backend implements SMTP server methods.
|
// The Backend implements SMTP server methods.
|
||||||
type Backend struct {
|
type Backend struct {
|
||||||
config *Config
|
config *smtp2shoutrrr.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +174,8 @@ type Session struct {
|
||||||
addresses []string
|
addresses []string
|
||||||
body string
|
body string
|
||||||
|
|
||||||
|
config *smtp2shoutrrr.Config
|
||||||
|
|
||||||
forwarderFunc func(ReceivedEmail) error
|
forwarderFunc func(ReceivedEmail) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,8 +188,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 != "username" || password != "password" {
|
if username != s.config.Username && password != s.config.Password {
|
||||||
return errors.New("Invalid username or password")
|
return fmt.Errorf("invalid credentials")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}), nil
|
}), nil
|
||||||
|
@ -256,7 +232,7 @@ func (s *Session) Logout() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var config *Config
|
var config *smtp2shoutrrr.Config
|
||||||
configPath := "config.toml"
|
configPath := "config.toml"
|
||||||
|
|
||||||
f, err := os.Open(configPath)
|
f, err := os.Open(configPath)
|
||||||
|
|
46
config.go
Normal file
46
config.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
|
Loading…
Add table
Add a link
Reference in a new issue