Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user