- 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
148 lines
2.7 KiB
Go
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
|
|
}
|