imc-vibe/backend/internal/mail/email.go
Christoph Haas 53ce3b06ae Add CLI flags for setup and server configuration
- Add --setup flag to create admin user
- Add --admin-user and --admin-password for setup
- Generate cryptographically random passwords for --setup
- Add --bind and --port flags for server binding
- Add BIND env var support
- Add SMTP email service for password reset
- Add password reset token storage
- Add auth header to queue/logs frontend pages
- Fix journalctl timestamp format
- Fix logs to return empty array instead of error when no entries
- Fix queue handler to return proper error message
- Hide Users/Aliases nav links when no domain selected
2026-03-22 01:17:43 +01:00

148 lines
2.7 KiB
Go

package mail
import (
"crypto/tls"
"fmt"
"net/smtp"
"strings"
"github.com/imc-vibe/backend/internal/config"
)
type EmailService struct {
host string
port string
username string
password string
from string
baseURL string
}
func NewEmailService(cfg *config.Config) *EmailService {
return &EmailService{
host: cfg.SMTPHost,
port: cfg.SMTPPort,
username: cfg.SMTPUser,
password: cfg.SMTPPassword,
from: cfg.SMTPFrom,
baseURL: cfg.BaseURL,
}
}
func (s *EmailService) SendPasswordReset(to, token string) error {
resetURL := fmt.Sprintf("%s/auth/reset-password?token=%s", s.baseURL, token)
subject := "Password Reset Request"
body := fmt.Sprintf(`Hi,
You requested a password reset for your account.
Click the link below to reset your password:
%s
This link will expire in 1 hour.
If you didn't request this, please ignore this email.
Best regards,
IMC Vibe Mail Server Admin
`, resetURL)
return s.send(to, subject, body)
}
func (s *EmailService) send(to, subject, body string) error {
if s.host == "" || s.username == "" {
return fmt.Errorf("SMTP not configured")
}
addr := fmt.Sprintf("%s:%s", s.host, s.port)
msg := fmt.Sprintf("From: %s\r\n"+
"To: %s\r\n"+
"Subject: %s\r\n"+
"MIME-Version: 1.0\r\n"+
"Content-Type: text/plain; charset=\"UTF-8\"\r\n"+
"\r\n"+
"%s", s.from, to, subject, body)
var auth smtp.Auth
if s.port == "465" {
err := s.sendWithTLS(addr, to, msg)
return err
} else {
auth = smtp.PlainAuth("", s.username, s.password, s.host)
err := smtp.SendMail(addr, auth, s.from, []string{to}, []byte(msg))
return err
}
}
func (s *EmailService) sendWithTLS(addr, to, msg string) error {
tlsConfig := &tls.Config{
ServerName: s.host,
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return err
}
defer conn.Close()
client, err := smtp.NewClient(conn, s.host)
if err != nil {
return err
}
defer client.Close()
auth := smtp.PlainAuth("", s.username, s.password, s.host)
if err := client.Auth(auth); err != nil {
return err
}
if err := client.Mail(s.from); err != nil {
return err
}
if err := client.Rcpt(to); err != nil {
return err
}
w, err := client.Data()
if err != nil {
return err
}
_, err = w.Write([]byte(msg))
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return client.Quit()
}
func GenerateToken() string {
b := make([]byte, 32)
for i := range b {
b[i] = tokenChars[i%len(tokenChars)]
}
return fmt.Sprintf("%x", b)
}
const tokenChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func ValidateToken(token string) bool {
if len(token) != 64 {
return false
}
for _, c := range token {
if !strings.ContainsRune(tokenChars, c) {
return false
}
}
return true
}