smtp2shoutrrr/config.go
Full-Stack Developer 8c12783e95
Some checks failed
CI / goreleaser-lint (pull_request) Failing after 37s
CI / format (pull_request) Successful in 1m20s
CI / test (pull_request) Successful in 2m36s
CI / lint (pull_request) Successful in 3m34s
CI / build (pull_request) Has been cancelled
deps: upgrade Go to 1.27.1, refresh dependencies and drop gotoolkit
Replaces the gotoolkit helpers with the standard library and the
libraries they wrapped:

- gotoolkit/encoding TOML wrapper -> pelletier/go-toml/v2 directly,
  behind a new smtp2shoutrrr.LoadConfig shared by both commands.
- gotoolkit/service + gotoolkit/model.Server -> signal.NotifyContext
  and a concrete *Server whose Start shuts the listener down
  gracefully when its context is cancelled.

Also drops the unused ANONYMOUS SASL client from cmd/sendmail, bumps
alpine, golangci-lint and the CI actions, and adds coverage for config
loading and the server lifecycle.

Closes FMG-2

Co-authored-by: multica-agent <github@multica.ai>
2026-09-07 16:28:11 +00:00

92 lines
2.1 KiB
Go

package smtp2shoutrrr
import (
"fmt"
"log/slog"
"net/url"
"os"
"strings"
"github.com/pelletier/go-toml/v2"
)
// LoadConfig reads the TOML configuration at path and applies its defaults.
func LoadConfig(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening config file %q: %w", path, err)
}
defer func() { _ = f.Close() }()
var config Config
if err := toml.NewDecoder(f).Decode(&config); err != nil {
return nil, fmt.Errorf("decoding config file %q: %w", path, err)
}
config.SetDefaults()
return &config, nil
}
type Config struct {
Port int
Username string
Password string
Recipients []ConfigRecipient
CatchAll *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 // deprecated: use Targets instead
Targets []string // shoutrrr addresses (supports multiple)
targetURLs []*url.URL // cached parsed URLs
}
func (cr *ConfigRecipient) GetTargetURLs() []*url.URL {
if cr.targetURLs == nil {
cr.targetURLs = make([]*url.URL, 0)
// Collect all targets, merging Target into Targets
allTargets := make([]string, 0)
// Handle deprecated Target field
if cr.Target != "" {
allTargets = append(allTargets, cr.Target)
slog.Warn("Target field is deprecated, use Targets instead",
slog.String("addresses", strings.Join(cr.Addresses, ",")))
}
// Add all Targets
allTargets = append(allTargets, cr.Targets...)
// Parse and cache all URLs
for _, target := range allTargets {
parsedURL, err := url.Parse(target)
if err != nil {
slog.Error("failed to parse shoutrrr target URL",
slog.String("target", target),
slog.String("err", err.Error()))
continue // Skip invalid URLs
}
cr.targetURLs = append(cr.targetURLs, parsedURL)
}
}
return cr.targetURLs
}