Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
package mail
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
)
|
|
|
|
// ErrNotConfigured is returned when a send is required but SMTP is not
|
|
// admin-configured (and no env fallback is available).
|
|
var ErrNotConfigured = errors.New("mail: SMTP is not configured; set platform mail settings in admin")
|
|
|
|
// ResolveFunc loads current SMTP config (typically from platformsettings).
|
|
// Callers must not log the returned password.
|
|
type ResolveFunc func() (Config, error)
|
|
|
|
// NewDynamic returns a Mailer that resolves SMTP config on each Send/Enabled.
|
|
// Prefer this over New(cfg) so admin dashboard changes apply without restart.
|
|
// When disabled or host empty, Send matches the historical no-op (log + nil)
|
|
// so invite re-issue can still mint tokens; callers that need hard failure
|
|
// should check Enabled() or use RequireConfigured.
|
|
func NewDynamic(resolve ResolveFunc) Mailer {
|
|
if resolve == nil {
|
|
return &noopMailer{}
|
|
}
|
|
return &dynamicMailer{resolve: resolve}
|
|
}
|
|
|
|
type dynamicMailer struct {
|
|
resolve ResolveFunc
|
|
}
|
|
|
|
func (d *dynamicMailer) load() (Config, error) {
|
|
cfg, err := d.resolve()
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if strings.TrimSpace(cfg.Port) == "" {
|
|
cfg.Port = "587"
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (d *dynamicMailer) Enabled() bool {
|
|
cfg, err := d.load()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return cfg.Enabled && strings.TrimSpace(cfg.Host) != ""
|
|
}
|
|
|
|
func (d *dynamicMailer) Send(msg Message) error {
|
|
cfg, err := d.load()
|
|
if err != nil {
|
|
log.Printf("mail: config resolve failed subject=%q", msg.Subject)
|
|
return fmt.Errorf("%w: %v", ErrNotConfigured, err)
|
|
}
|
|
if !cfg.Enabled || strings.TrimSpace(cfg.Host) == "" {
|
|
return (&noopMailer{}).Send(msg)
|
|
}
|
|
return (&smtpMailer{cfg: cfg}).Send(msg)
|
|
}
|
|
|
|
// RequireConfigured wraps a Mailer so Send fails clearly when delivery is off.
|
|
func RequireConfigured(inner Mailer) Mailer {
|
|
if inner == nil {
|
|
return &requireConfiguredMailer{inner: &noopMailer{}}
|
|
}
|
|
return &requireConfiguredMailer{inner: inner}
|
|
}
|
|
|
|
type requireConfiguredMailer struct {
|
|
inner Mailer
|
|
}
|
|
|
|
func (r *requireConfiguredMailer) Enabled() bool { return r.inner.Enabled() }
|
|
|
|
func (r *requireConfiguredMailer) Send(msg Message) error {
|
|
if !r.inner.Enabled() {
|
|
return ErrNotConfigured
|
|
}
|
|
return r.inner.Send(msg)
|
|
}
|
|
|
|
// ConfigFromParts builds a Config from discrete fields (platformsettings bridge).
|
|
func ConfigFromParts(enabled bool, host, port, user, password, from string) Config {
|
|
if strings.TrimSpace(port) == "" {
|
|
port = "587"
|
|
}
|
|
return Config{
|
|
Enabled: enabled,
|
|
Host: strings.TrimSpace(host),
|
|
Port: strings.TrimSpace(port),
|
|
User: strings.TrimSpace(user),
|
|
Password: password,
|
|
From: strings.TrimSpace(from),
|
|
}
|
|
}
|
|
|
|
// ApplyDryRun forces Enabled=false when dry-run is on so New/NewDynamic use the
|
|
// noop path (log subject only). Host/from are preserved for diagnostics.
|
|
func ApplyDryRun(dryRun bool, cfg Config) Config {
|
|
if dryRun {
|
|
cfg.Enabled = false
|
|
}
|
|
return cfg
|
|
}
|