bfa8797f8c
Engine: multi-player drop-out-and-continue with a per-game tile disposition (remove default / return), resigned seats skipped and excluded from the win, leaver rack never revealed; 2-player behaviour unchanged. New domains (service/store, no HTTP yet): internal/social (friend request/accept graph, per-user blocks, per-game chat with nudge as a message kind, content filter via mvdan.cc/xurls/v2 + leet/separator normaliser + phone heuristic) and internal/lobby (in-memory variant-keyed matchmaking pool, friend-game invitations invite->accept with lazy 7-day expiry). account gains profile editing and the email confirm-code flow (Mailer seam: SMTP or log mailer). Migration 00003_social.sql + regenerated jet. main wires the new services into the server (accessors for the Stage 6 handlers); robot substitution stays in Stage 5, REST/stream/push in Stage 6/8. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, README) updated.
85 lines
2.6 KiB
Go
85 lines
2.6 KiB
Go
package account
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/smtp"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Mailer delivers a transactional email. It is the seam behind which the email
|
|
// confirm-code flow sends codes, so the relay is swappable and unit tests use a
|
|
// fixture (see docs/TESTING.md: no real network in tests). The context is offered
|
|
// for cancellation; the standard-library SMTP implementation sends synchronously
|
|
// and ignores it.
|
|
type Mailer interface {
|
|
Send(ctx context.Context, to, subject, body string) error
|
|
}
|
|
|
|
// 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).
|
|
type SMTPConfig struct {
|
|
Host string
|
|
Port string
|
|
Username string
|
|
Password string
|
|
From string
|
|
}
|
|
|
|
// SMTPMailer sends mail through an SMTP relay using the standard library. When a
|
|
// username is set it authenticates with PLAIN; otherwise it relays unauthenticated.
|
|
type SMTPMailer struct {
|
|
cfg SMTPConfig
|
|
}
|
|
|
|
// NewSMTPMailer constructs an SMTPMailer for cfg.
|
|
func NewSMTPMailer(cfg SMTPConfig) SMTPMailer {
|
|
return SMTPMailer{cfg: cfg}
|
|
}
|
|
|
|
// Send delivers a plain-text UTF-8 message to to via the configured relay.
|
|
func (m SMTPMailer) Send(_ context.Context, to, subject, body string) error {
|
|
addr := net.JoinHostPort(m.cfg.Host, m.cfg.Port)
|
|
var auth smtp.Auth
|
|
if m.cfg.Username != "" {
|
|
auth = smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
|
|
}
|
|
if err := smtp.SendMail(addr, auth, m.cfg.From, []string{to}, message(m.cfg.From, to, subject, body)); err != nil {
|
|
return fmt.Errorf("account: send mail to %s: %w", to, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// message renders a minimal RFC 5322 plain-text email.
|
|
func message(from, to, subject, body string) []byte {
|
|
return []byte("From: " + from + "\r\n" +
|
|
"To: " + to + "\r\n" +
|
|
"Subject: " + subject + "\r\n" +
|
|
"MIME-Version: 1.0\r\n" +
|
|
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
|
"\r\n" + body + "\r\n")
|
|
}
|
|
|
|
// LogMailer logs the message instead of sending it. It is the default when no
|
|
// SMTP relay is configured and is intended for development only: it logs the body,
|
|
// which carries the confirm-code, so it must not be used in production.
|
|
type LogMailer struct {
|
|
log *zap.Logger
|
|
}
|
|
|
|
// NewLogMailer constructs a LogMailer that logs through log.
|
|
func NewLogMailer(log *zap.Logger) LogMailer {
|
|
return LogMailer{log: log}
|
|
}
|
|
|
|
// Send logs the message at info level and reports success.
|
|
func (m LogMailer) Send(_ context.Context, to, subject, body string) error {
|
|
if m.log != nil {
|
|
m.log.Info("email not sent (log mailer)",
|
|
zap.String("to", to), zap.String("subject", subject), zap.String("body", body))
|
|
}
|
|
return nil
|
|
}
|