package auth import ( "context" "fmt" "io" "net/smtp" "strings" ) // Mailer sends magic-link emails. The interface keeps the auth flow // decoupled from any concrete delivery backend. type Mailer interface { SendMagicLink(ctx context.Context, email, link string) error } // LogMailer writes the magic link to a writer instead of sending an // email. Useful for development and tests. type LogMailer struct{ W io.Writer } func (m LogMailer) SendMagicLink(_ context.Context, email, link string) error { if m.W == nil { return nil } _, err := fmt.Fprintf(m.W, "magic link for %s: %s\n", email, link) return err } // SMTPMailer delivers via plain SMTP with optional auth. TLS is the // caller's responsibility (use submission port 587 with STARTTLS or 465 // with implicit TLS — the stdlib smtp package handles STARTTLS via // SendMail when the server supports it). type SMTPMailer struct { Host string Port string Username string Password string From string } func (m SMTPMailer) SendMagicLink(_ context.Context, to, link string) error { if m.Host == "" || m.From == "" { return fmt.Errorf("smtp not configured") } addr := m.Host + ":" + m.Port var auth smtp.Auth if m.Username != "" { auth = smtp.PlainAuth("", m.Username, m.Password, m.Host) } subject := "Your librenotes login link" body := fmt.Sprintf("Click to sign in to librenotes:\n\n%s\n\nThis link is valid for 15 minutes and can be used once.\n", link) msg := strings.Join([]string{ "From: " + m.From, "To: " + to, "Subject: " + subject, "MIME-Version: 1.0", "Content-Type: text/plain; charset=utf-8", "", body, }, "\r\n") return smtp.SendMail(addr, auth, m.From, []string{to}, []byte(msg)) }