Password email not sent (#34)

* Sending mails from password reset

now we are really sending the mail
* Must set Mail configuration
This commit is contained in:
Bardioc26
2026-02-17 23:12:58 +01:00
committed by GitHub
parent 1084a16eae
commit a7eb3cda81
6 changed files with 425 additions and 20 deletions
+34
View File
@@ -32,6 +32,13 @@ type Config struct {
// PDF Templates
TemplatesDir string // Directory where PDF templates are stored
ExportTempDir string // Directory for temporary PDF exports
// Mail Configuration
MailHost string // SMTP server host
MailPort int // SMTP server port
MailUsername string // SMTP username
MailPassword string // SMTP password
MailFrom string // Default sender email address
}
// Cfg ist die globale Konfigurationsvariable
@@ -56,6 +63,11 @@ func defaultConfig() *Config {
FrontendURL: "http://localhost:5173", // Default frontend URL for development
TemplatesDir: "./templates", // Default templates directory
ExportTempDir: "./xporttemp", // Default export temp directory
MailHost: "", // No default, must be configured
MailPort: 465, // Default SMTP SSL port
MailUsername: "", // No default, must be configured
MailPassword: "", // No default, must be configured
MailFrom: "", // No default, must be configured
}
}
@@ -136,6 +148,28 @@ func LoadConfig() *Config {
config.ExportTempDir = exportTempDir
}
// Mail Configuration
if mailHost := os.Getenv("MAIL_HOST"); mailHost != "" {
config.MailHost = mailHost
}
if mailPort := os.Getenv("MAIL_PORT"); mailPort != "" {
if port, err := strconv.Atoi(mailPort); err == nil {
config.MailPort = port
}
}
if mailUsername := os.Getenv("MAIL_USERNAME"); mailUsername != "" {
config.MailUsername = mailUsername
}
if mailPassword := os.Getenv("MAIL_PASSWORD"); mailPassword != "" {
config.MailPassword = mailPassword
}
if mailFrom := os.Getenv("MAIL_FROM"); mailFrom != "" {
config.MailFrom = mailFrom
} else if config.MailUsername != "" {
// Fallback: Verwende Username als From-Adresse
config.MailFrom = config.MailUsername
}
fmt.Printf("DEBUG LoadConfig - Finale Config: Environment='%s', DevTesting='%s', DatabaseType='%s'\n Complete: %v\n",
config.Environment, config.DevTesting, config.DatabaseType, config)
+163
View File
@@ -0,0 +1,163 @@
package mail
import (
"crypto/tls"
"fmt"
"net/smtp"
"bamort/config"
"bamort/logger"
)
// Client represents an SMTP mail client
type Client struct {
host string
port int
username string
password string
from string
}
// Message represents an email message
type Message struct {
To string
Subject string
Body string
}
// NewClient creates a new SMTP mail client from config
func NewClient() *Client {
cfg := config.Cfg
return &Client{
host: cfg.MailHost,
port: cfg.MailPort,
username: cfg.MailUsername,
password: cfg.MailPassword,
from: cfg.MailFrom,
}
}
// Send sends an email message via SMTP
func (c *Client) Send(msg Message) error {
if c.host == "" {
logger.Warn("SMTP Host nicht konfiguriert - E-Mail-Versand übersprungen")
return fmt.Errorf("SMTP host not configured")
}
logger.Debug("Sende E-Mail an %s via SMTP %s:%d", msg.To, c.host, c.port)
// Prepare email headers and body
headers := make(map[string]string)
headers["From"] = c.from
headers["To"] = msg.To
headers["Subject"] = msg.Subject
headers["MIME-Version"] = "1.0"
headers["Content-Type"] = "text/html; charset=\"UTF-8\""
// Build message
message := ""
for key, value := range headers {
message += fmt.Sprintf("%s: %s\r\n", key, value)
}
message += "\r\n" + msg.Body
// Connect to SMTP server
serverAddr := fmt.Sprintf("%s:%d", c.host, c.port)
// Create TLS config
tlsConfig := &tls.Config{
ServerName: c.host,
InsecureSkipVerify: false,
}
var err error
var client *smtp.Client
// Port 465 requires direct TLS connection, port 587 uses STARTTLS
if c.port == 465 {
// Direct TLS connection (implicit SSL)
conn, err := tls.Dial("tcp", serverAddr, tlsConfig)
if err != nil {
logger.Error("Fehler beim Verbinden mit SMTP-Server (TLS): %s", err.Error())
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer conn.Close()
client, err = smtp.NewClient(conn, c.host)
if err != nil {
logger.Error("Fehler beim Erstellen des SMTP-Clients: %s", err.Error())
return fmt.Errorf("failed to create SMTP client: %w", err)
}
} else {
// Port 587 or other - use STARTTLS
client, err = smtp.Dial(serverAddr)
if err != nil {
logger.Error("Fehler beim Verbinden mit SMTP-Server: %s", err.Error())
return fmt.Errorf("failed to dial SMTP server: %w", err)
}
defer client.Close()
// Start TLS if available
if ok, _ := client.Extension("STARTTLS"); ok {
if err = client.StartTLS(tlsConfig); err != nil {
logger.Error("Fehler beim STARTTLS: %s", err.Error())
return fmt.Errorf("failed to start TLS: %w", err)
}
}
}
// Authenticate
if c.username != "" && c.password != "" {
auth := smtp.PlainAuth("", c.username, c.password, c.host)
if err = client.Auth(auth); err != nil {
logger.Error("Fehler bei der SMTP-Authentifizierung: %s", err.Error())
return fmt.Errorf("failed to authenticate: %w", err)
}
}
// Set sender
if err = client.Mail(c.from); err != nil {
logger.Error("Fehler beim Setzen des Absenders: %s", err.Error())
return fmt.Errorf("failed to set sender: %w", err)
}
// Set recipient
if err = client.Rcpt(msg.To); err != nil {
logger.Error("Fehler beim Setzen des Empfängers: %s", err.Error())
return fmt.Errorf("failed to set recipient: %w", err)
}
// Send message body
writer, err := client.Data()
if err != nil {
logger.Error("Fehler beim Öffnen des Data-Writers: %s", err.Error())
return fmt.Errorf("failed to open data writer: %w", err)
}
_, err = writer.Write([]byte(message))
if err != nil {
logger.Error("Fehler beim Schreiben der Nachricht: %s", err.Error())
writer.Close()
return fmt.Errorf("failed to write message: %w", err)
}
err = writer.Close()
if err != nil {
logger.Error("Fehler beim Schließen des Data-Writers: %s", err.Error())
return fmt.Errorf("failed to close data writer: %w", err)
}
// Quit
if err = client.Quit(); err != nil {
logger.Error("Fehler beim Beenden der SMTP-Verbindung: %s", err.Error())
return fmt.Errorf("failed to quit SMTP connection: %w", err)
}
logger.Info("E-Mail erfolgreich an %s versendet", msg.To)
return nil
}
// IsConfigured returns true if the mail client is properly configured
func (c *Client) IsConfigured() bool {
return c.host != "" && c.port > 0 && c.from != ""
}
+140
View File
@@ -0,0 +1,140 @@
package mail
import (
"os"
"testing"
"bamort/config"
)
// setupTestEnvironment setzt die Test-Umgebung auf
func setupTestEnvironment(t *testing.T) {
original := os.Getenv("ENVIRONMENT")
os.Setenv("ENVIRONMENT", "test")
t.Cleanup(func() {
if original != "" {
os.Setenv("ENVIRONMENT", original)
} else {
os.Unsetenv("ENVIRONMENT")
}
})
// Reload config with test environment
config.Cfg = config.LoadConfig()
}
func TestNewClient(t *testing.T) {
setupTestEnvironment(t)
// Set test mail config
os.Setenv("MAIL_HOST", "smtp.example.com")
os.Setenv("MAIL_PORT", "465")
os.Setenv("MAIL_USERNAME", "test@example.com")
os.Setenv("MAIL_PASSWORD", "testpass")
os.Setenv("MAIL_FROM", "sender@example.com")
// Reload config
config.Cfg = config.LoadConfig()
client := NewClient()
if client.host != "smtp.example.com" {
t.Errorf("Expected host 'smtp.example.com', got '%s'", client.host)
}
if client.port != 465 {
t.Errorf("Expected port 465, got %d", client.port)
}
if client.username != "test@example.com" {
t.Errorf("Expected username 'test@example.com', got '%s'", client.username)
}
if client.from != "sender@example.com" {
t.Errorf("Expected from 'sender@example.com', got '%s'", client.from)
}
}
func TestIsConfigured(t *testing.T) {
tests := []struct {
name string
client *Client
expected bool
}{
{
name: "Fully configured",
client: &Client{
host: "smtp.example.com",
port: 465,
from: "test@example.com",
},
expected: true,
},
{
name: "Missing host",
client: &Client{
host: "",
port: 465,
from: "test@example.com",
},
expected: false,
},
{
name: "Missing port",
client: &Client{
host: "smtp.example.com",
port: 0,
from: "test@example.com",
},
expected: false,
},
{
name: "Missing from",
client: &Client{
host: "smtp.example.com",
port: 465,
from: "",
},
expected: false,
},
{
name: "Empty client",
client: &Client{
host: "",
port: 0,
from: "",
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.client.IsConfigured()
if result != tt.expected {
t.Errorf("Expected IsConfigured() to return %v, got %v", tt.expected, result)
}
})
}
}
func TestSend_NotConfigured(t *testing.T) {
setupTestEnvironment(t)
// Create client without config
client := &Client{
host: "",
port: 0,
from: "",
}
msg := Message{
To: "recipient@example.com",
Subject: "Test",
Body: "Test body",
}
err := client.Send(msg)
if err == nil {
t.Error("Expected error when sending with unconfigured client, got nil")
}
if err.Error() != "SMTP host not configured" {
t.Errorf("Expected error 'SMTP host not configured', got '%s'", err.Error())
}
}
+75 -19
View File
@@ -7,6 +7,7 @@ package user
import (
"bamort/logger"
"bamort/mail"
"crypto/md5"
"crypto/rand"
"encoding/hex"
@@ -239,8 +240,7 @@ func generateResetHash() (string, error) {
return hex.EncodeToString(bytes), nil
}
// sendResetEmail simuliert das Senden einer E-Mail (hier nur Logging)
// In einer echten Implementierung würde hier ein E-Mail-Service verwendet
// sendResetEmail sends a password reset email via SMTP
func sendResetEmail(email, username, resetHash, frontendURL string) error {
// Verwende die mitgegebene Frontend-URL oder fallback auf Standard
baseURL := frontendURL
@@ -250,25 +250,81 @@ func sendResetEmail(email, username, resetHash, frontendURL string) error {
resetLink := fmt.Sprintf("%s/reset-password?token=%s", baseURL, resetHash)
logger.Info("=== PASSWORD RESET EMAIL ===")
logger.Info("An: %s", email)
logger.Info("Betreff: Passwort zurücksetzen für %s", username)
logger.Info("Nachricht:")
logger.Info("Hallo %s,", username)
logger.Info("")
logger.Info("Sie haben eine Passwort-Zurücksetzung angefordert.")
logger.Info("Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:")
logger.Info("")
logger.Info("%s", resetLink)
logger.Info("")
logger.Info("Dieser Link ist 14 Tage gültig.")
logger.Info("Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.")
logger.Info("")
logger.Info("=== END EMAIL ===")
// Create mail client
mailClient := mail.NewClient()
// TODO: Hier echte E-Mail-Integration hinzufügen
// z.B. SendGrid, SMTP, etc.
// If mail is not configured, fallback to logging
if !mailClient.IsConfigured() {
logger.Warn("SMTP nicht konfiguriert - E-Mail wird nur geloggt")
logger.Info("=== PASSWORD RESET EMAIL ===")
logger.Info("An: %s", email)
logger.Info("Betreff: Passwort zurücksetzen für %s", username)
logger.Info("Nachricht:")
logger.Info("Hallo %s,", username)
logger.Info("")
logger.Info("Sie haben eine Passwort-Zurücksetzung angefordert.")
logger.Info("Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:")
logger.Info("")
logger.Info("%s", resetLink)
logger.Info("")
logger.Info("Dieser Link ist 14 Tage gültig.")
logger.Info("Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.")
logger.Info("")
logger.Info("=== END EMAIL ===")
return nil
}
// Build HTML email body
htmlBody := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
.content { background-color: #f9f9f9; padding: 30px; margin-top: 20px; }
.button { display: inline-block; padding: 12px 30px; margin: 20px 0; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Passwort zurücksetzen</h1>
</div>
<div class="content">
<p>Hallo %s,</p>
<p>Sie haben eine Passwort-Zurücksetzung für Ihren Bamort-Account angefordert.</p>
<p>Klicken Sie auf den folgenden Button, um Ihr Passwort zurückzusetzen:</p>
<p style="text-align: center;">
<a href="%s" class="button">Passwort zurücksetzen</a>
</p>
<p>Oder kopieren Sie diesen Link in Ihren Browser:</p>
<p style="word-break: break-all; color: #666;">%s</p>
<p><strong>Dieser Link ist 14 Tage gültig.</strong></p>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren. Ihr Passwort bleibt unverändert.</p>
</div>
<div class="footer">
<p>Dies ist eine automatisch generierte E-Mail. Bitte antworten Sie nicht auf diese Nachricht.</p>
</div>
</div>
</body>
</html>`, username, resetLink, resetLink)
// Send email
msg := mail.Message{
To: email,
Subject: "Passwort zurücksetzen - Bamort",
Body: htmlBody,
}
if err := mailClient.Send(msg); err != nil {
logger.Error("Fehler beim Versenden der Reset-E-Mail an %s: %s", email, err.Error())
return err
}
logger.Info("Password-Reset-E-Mail erfolgreich an %s versendet", email)
return nil
}