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>
92 lines
2.1 KiB
Go
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
|
|
}
|