smtp2shoutrrr/cmd/sendmail/main.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

76 lines
2 KiB
Go

package main
import (
"errors"
"fmt"
"log/slog"
"net/smtp"
"os"
"git.nakama.town/fmartingr/smtp2shoutrrr"
)
// The PLAIN mechanism name.
const Plain = "PLAIN"
type plainClient struct {
Identity string
Username string
Password string
}
func (a *plainClient) Start(si *smtp.ServerInfo) (mech string, ir []byte, err error) {
mech = Plain
ir = []byte(a.Identity + "\x00" + a.Username + "\x00" + a.Password)
return
}
func (a *plainClient) Next(challenge []byte, b bool) (response []byte, err error) {
slog.Debug("SASL challenge received", slog.String("challenge", string(challenge)))
return nil, nil
}
// NewPlainClient is a client implementation of the PLAIN authentication
// mechanism, as described in RFC 4616. Unlike smtp.PlainAuth it does not
// require a TLS connection, which the development server does not offer.
// Authorization identity may be left blank to indicate that it is the same as
// the username.
func NewPlainClient(identity, username, password string) smtp.Auth {
return &plainClient{identity, username, password}
}
const configPath = "config.toml"
func main() {
if err := run(); err != nil {
slog.Error("sendmail failed", slog.String("err", err.Error()))
os.Exit(1)
}
}
func run() error {
config, err := smtp2shoutrrr.LoadConfig(configPath)
if err != nil {
return fmt.Errorf("loading configuration: %w", err)
}
// The development server listens on localhost without TLS.
hostname := "localhost"
auth := NewPlainClient("", config.Username, config.Password)
slog.Info("Using first recipient configuration to send a test email")
if len(config.Recipients) == 0 {
return errors.New("no recipients found in configuration")
}
if len(config.Recipients[0].Addresses) == 0 {
return errors.New("no email addresses found in first recipient configuration")
}
recipients := []string{config.Recipients[0].Addresses[0]}
msg := []byte("Subject: Test notification\r\n\r\nThis is a test notification")
from := "hello@localhost"
return smtp.SendMail(fmt.Sprintf("%s:%d", hostname, config.Port), auth, from, recipients, msg)
}