feat(adminalert): operator email on new feedback / word complaints
New adminalert worker polls for feedback + word complaints arriving since the last check and coalesces a burst into one digest email per interval (5 min), inert unless a distinct admin sender (BACKEND_SMTP_ADMIN_FROM) and recipient (BACKEND_ADMIN_EMAIL, comma-separated allowed) are configured. The mailer gains a per-message From override and splits a comma-separated To into separate recipients (go-mail needs them as a list). Feedback/game stores gain CountSince/CountComplaintsSince. Unit tests cover the digest, the skip-when- empty, and the recipient split.
This commit is contained in:
@@ -15,7 +15,11 @@ import (
|
||||
// required plain-text body and doubles as the multipart/alternative fallback;
|
||||
// HTML, when non-empty, is the preferred body a capable client renders instead.
|
||||
type Message struct {
|
||||
To string
|
||||
// To is the recipient address, or several comma-separated (all get the one message).
|
||||
To string
|
||||
// From, when non-empty, overrides the configured sender for this message — the admin
|
||||
// alert path uses a distinct From from the user-facing confirm-code sender.
|
||||
From string
|
||||
Subject string
|
||||
Text string
|
||||
HTML string
|
||||
@@ -29,6 +33,18 @@ type Mailer interface {
|
||||
Send(ctx context.Context, msg Message) error
|
||||
}
|
||||
|
||||
// splitAddrs splits a comma-separated recipient list into trimmed, non-empty addresses.
|
||||
func splitAddrs(list string) []string {
|
||||
parts := strings.Split(list, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if a := strings.TrimSpace(p); a != "" {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SMTPConfig configures the SMTP relay. An empty Host selects the LogMailer
|
||||
// instead, so a deployment without a relay still runs (the code lands in the log).
|
||||
// TLS is always used and no client certificate is required — only the server
|
||||
@@ -44,6 +60,11 @@ type SMTPConfig struct {
|
||||
// port (implicit TLS on 465, STARTTLS otherwise); set it explicitly for a relay on
|
||||
// a non-standard port (e.g. Selectel's 1127 = SSL, 1126 = STARTTLS).
|
||||
TLS string
|
||||
// AdminFrom / AdminTo drive the operator alert emails (new feedback / word complaints),
|
||||
// distinct from the user-facing confirm-code sender. AdminTo may be several
|
||||
// comma-separated addresses. Both empty disables the alert worker.
|
||||
AdminFrom string
|
||||
AdminTo string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -110,10 +131,16 @@ func (m SMTPMailer) Send(ctx context.Context, msg Message) error {
|
||||
return fmt.Errorf("account: build mail client: %w", err)
|
||||
}
|
||||
out := mail.NewMsg()
|
||||
if err := out.From(m.cfg.From); err != nil {
|
||||
return fmt.Errorf("account: set From %q: %w", m.cfg.From, err)
|
||||
from := m.cfg.From
|
||||
if msg.From != "" {
|
||||
from = msg.From
|
||||
}
|
||||
if err := out.To(msg.To); err != nil {
|
||||
if err := out.From(from); err != nil {
|
||||
return fmt.Errorf("account: set From %q: %w", from, err)
|
||||
}
|
||||
// To may carry several comma-separated recipients; go-mail wants them as separate
|
||||
// arguments (a single joined string parses as one malformed address).
|
||||
if err := out.To(splitAddrs(msg.To)...); err != nil {
|
||||
return fmt.Errorf("account: set To %q: %w", msg.To, err)
|
||||
}
|
||||
out.Subject(msg.Subject)
|
||||
|
||||
Reference in New Issue
Block a user