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/
|
||||
|
||||
# smtp2shoutrrr
|
||||
config.toml
|
||||
|
|
|
@ -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 <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:
|
||||
- enabled: true
|
||||
ids:
|
||||
|
|
53
README.md
53
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
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