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
This commit is contained in:
parent
2c3a4c3daa
commit
53ce3b06ae
14 changed files with 435 additions and 69 deletions
148
backend/internal/mail/email.go
Normal file
148
backend/internal/mail/email.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ func GetLogs(hours int, filter string) ([]LogEntry, error) {
|
|||
"-u", "postfix",
|
||||
"--since", formatDuration(hours),
|
||||
"--no-pager",
|
||||
"-o", "short=false",
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "journalctl", args...)
|
||||
|
|
@ -40,7 +39,11 @@ func GetLogs(hours int, filter string) ([]LogEntry, error) {
|
|||
priorityRegex := regexp.MustCompile(`\[([^\]]+)\]`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if strings.Contains(line, "-- No entries") || line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if filter != "" && !strings.Contains(strings.ToLower(line), strings.ToLower(filter)) {
|
||||
continue
|
||||
|
|
@ -84,5 +87,5 @@ func GetLogs(hours int, filter string) ([]LogEntry, error) {
|
|||
}
|
||||
|
||||
func formatDuration(hours int) string {
|
||||
return time.Now().Add(-time.Duration(hours) * time.Hour).Format("15:04:05 2006-01-02")
|
||||
return time.Now().Add(-time.Duration(hours) * time.Hour).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue