feat(email): SMTP welcome email on user creation

This commit is contained in:
Claus Lohmar 2026-06-15 06:38:11 +00:00
parent 8bbdaf54ac
commit 3b3cc7baa4
4 changed files with 101 additions and 2 deletions

View file

@ -5,6 +5,8 @@ import (
"log/slog"
"net/http"
"strings"
"git.lohmar.co.uk/lexton-it/NextWks/core/email"
)
// Handler bundles admin HTTP handlers and their dependencies.
@ -12,15 +14,17 @@ type Handler struct {
store *UserStore
groupStore *GroupStore
syncWriter *SyncWriter
emailer *email.Sender
logger *slog.Logger
configPath string
}
func NewHandler(store *UserStore, groupStore *GroupStore, syncWriter *SyncWriter, logger *slog.Logger, configPath string) *Handler {
func NewHandler(store *UserStore, groupStore *GroupStore, syncWriter *SyncWriter, emailer *email.Sender, logger *slog.Logger, configPath string) *Handler {
return &Handler{
store: store,
groupStore: groupStore,
syncWriter: syncWriter,
emailer: emailer,
logger: logger,
configPath: configPath,
}

View file

@ -283,6 +283,21 @@ func (h *Handler) createUsersHandler(w http.ResponseWriter, r *http.Request) {
// Sync to Authelia YAML
h.syncWriter.Sync()
// Send welcome emails
for _, r := range results {
if r.Error == "" && r.GeneratedPassword != "" {
email := ""
for _, input := range req.Users {
if input.Username == r.Username {
email = input.Email
}
}
if email != "" {
go h.emailer.SendWelcome(email, r.Username, r.GeneratedPassword, "") // workspace URL from config
}
}
}
// Convert to template results
resultRows := make([]templates.CreateUserResultRow, 0, len(results))
for _, r := range results {

76
src/core/email/email.go Normal file
View file

@ -0,0 +1,76 @@
package email
import (
"fmt"
"log/slog"
"net/smtp"
"strings"
)
// Sender handles SMTP email delivery.
type Sender struct {
host string
port string
username string
password string
from string
logger *slog.Logger
}
// NewSender creates an email sender from SMTP config.
func NewSender(host string, port int, username, password, from string, logger *slog.Logger) *Sender {
return &Sender{
host: host,
port: fmt.Sprintf("%d", port),
username: username,
password: password,
from: from,
logger: logger,
}
}
// SendWelcome sends a welcome email with the generated password.
func (s *Sender) SendWelcome(to, username, password, workspaceURL string) error {
subject := "Welcome to Next Workspace"
body := fmt.Sprintf(`Hello %s,
Your Next Workspace account has been created.
Workspace: %s
Username: %s
Password: %s
Please log in and change your password.
Best regards,
Next Workspace`, username, workspaceURL, username, password)
return s.send(to, subject, body)
}
// SendAlert sends a generic alert email.
func (s *Sender) SendAlert(to, subject, message string) error {
return s.send(to, subject, message)
}
func (s *Sender) send(to, subject, body string) error {
if s.host == "" || s.username == "" {
s.logger.Warn("email not configured, skipping send", "to", to)
return nil
}
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
s.from, to, subject, body)
addr := fmt.Sprintf("%s:%s", s.host, s.port)
auth := smtp.PlainAuth("", s.username, s.password, s.host)
err := smtp.SendMail(addr, auth, s.from, strings.Split(to, ","), []byte(msg))
if err != nil {
s.logger.Error("send email failed", "to", to, "error", err)
return fmt.Errorf("send email: %w", err)
}
s.logger.Info("email sent", "to", to, "subject", subject)
return nil
}

View file

@ -16,6 +16,7 @@ import (
"git.lohmar.co.uk/lexton-it/NextWks/core/auth"
"git.lohmar.co.uk/lexton-it/NextWks/core/config"
"git.lohmar.co.uk/lexton-it/NextWks/core/db"
"git.lohmar.co.uk/lexton-it/NextWks/core/email"
"git.lohmar.co.uk/lexton-it/NextWks/core/ui"
"git.lohmar.co.uk/lexton-it/NextWks/core/version"
)
@ -70,8 +71,11 @@ func main() {
logger.Info("fixed user roles", "count", fixed)
}
// Create email sender
emailSender := email.NewSender(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.From, logger)
// Create admin handler
adminHandler := admin.NewHandler(userStore, admin.NewGroupStore(database.DB), syncWriter, logger, *configPath)
adminHandler := admin.NewHandler(userStore, admin.NewGroupStore(database.DB), syncWriter, emailSender, logger, *configPath)
// Initialize session store and OIDC auth
sessionStore := auth.NewSessionStore(database.DB)