server and web app

This commit is contained in:
2026-05-31 23:08:53 -04:00
parent 438b3b29a2
commit b3364447a1
24 changed files with 2318 additions and 153 deletions

View File

@@ -0,0 +1,67 @@
package email
import (
"context"
"fmt"
"log/slog"
"net/smtp"
"github.com/tim/cairnquire/apps/server/internal/config"
)
type Sender interface {
Send(ctx context.Context, to []string, subject, body string) error
}
type SMTPConfig struct {
Host string
Port int
From string
}
type SMTPSender struct {
cfg SMTPConfig
logger *slog.Logger
}
func NewSMTPSender(cfg config.EmailConfig, logger *slog.Logger) *SMTPSender {
return &SMTPSender{
cfg: SMTPConfig{
Host: cfg.SMTPHost,
Port: cfg.SMTPPort,
From: cfg.From,
},
logger: logger,
}
}
func (s *SMTPSender) Send(ctx context.Context, to []string, subject, body string) error {
if s.cfg.Host == "" {
return fmt.Errorf("email host not configured")
}
addr := fmt.Sprintf("%s:%d", s.cfg.Host, s.cfg.Port)
msg := []byte(fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s\r\n", to[0], subject, body))
s.logger.Info("sending email", "to", to, "subject", subject, "addr", addr)
// Use SMTP without auth for local testing (Mailpit/MailHog)
var auth smtp.Auth
err := smtp.SendMail(addr, auth, s.cfg.From, to, msg)
if err != nil {
return fmt.Errorf("send email: %w", err)
}
return nil
}
type NoOpSender struct {
logger *slog.Logger
}
func NewNoOpSender(logger *slog.Logger) *NoOpSender {
return &NoOpSender{logger: logger}
}
func (n *NoOpSender) Send(ctx context.Context, to []string, subject, body string) error {
n.logger.Info("email noop", "to", to, "subject", subject, "body_len", len(body))
return nil
}