78b57273e2
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
336 lines
8.6 KiB
Go
336 lines
8.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// DockerMailMessage represents an email read via docker exec
|
|
type DockerMailMessage struct {
|
|
UID uint32 `json:"uid"`
|
|
Subject string `json:"subject"`
|
|
From string `json:"from"`
|
|
To []string `json:"to"`
|
|
Date string `json:"date"`
|
|
Body string `json:"body"`
|
|
Preview string `json:"preview"`
|
|
Read bool `json:"read"`
|
|
Attachments int `json:"attachments"`
|
|
}
|
|
|
|
// getMaildirViaDocker returns the maildir path inside the container
|
|
func getMaildirViaDocker(email string) string {
|
|
return fmt.Sprintf("/mail/%s", email)
|
|
}
|
|
|
|
// listMaildirViaDocker lists messages using docker exec
|
|
func listMaildirViaDocker(email string, limit int) ([]DockerMailMessage, error) {
|
|
maildir := getMaildirViaDocker(email)
|
|
|
|
// List all files in cur/ and new/
|
|
cmd := exec.Command("sh", "-c", fmt.Sprintf("docker exec mailu-imap-1 find %s/cur %s/new -type f 2>/dev/null || true", maildir, maildir))
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list maildir: %w", err)
|
|
}
|
|
|
|
files := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
var messages []DockerMailMessage
|
|
|
|
for _, file := range files {
|
|
if file == "" {
|
|
continue
|
|
}
|
|
|
|
msg, err := readMailFileViaDocker(file)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
messages = append(messages, *msg)
|
|
}
|
|
|
|
// Sort by date (newest first) - simplified
|
|
// In real implementation, parse dates properly
|
|
|
|
if limit > 0 && len(messages) > limit {
|
|
messages = messages[:limit]
|
|
}
|
|
|
|
return messages, nil
|
|
}
|
|
|
|
// readMailFileViaDocker reads a single mail file via docker exec
|
|
func readMailFileViaDocker(path string) (*DockerMailMessage, error) {
|
|
cmd := exec.Command("docker", "exec", "mailu-imap-1", "cat", path)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use proper MIME parser
|
|
parsed, err := ParseEmail(output)
|
|
if err != nil {
|
|
// Fallback to simple parsing
|
|
parsed = parseSimple(output)
|
|
}
|
|
|
|
msg := &DockerMailMessage{
|
|
Read: strings.Contains(path, "/cur/"),
|
|
Attachments: parsed.Attachments,
|
|
Subject: parsed.Subject,
|
|
From: parsed.From,
|
|
To: parsed.To,
|
|
Date: parsed.Date,
|
|
Body: parsed.Body,
|
|
Preview: parsed.Preview,
|
|
}
|
|
|
|
// Generate UID from filename
|
|
filename := path[strings.LastIndex(path, "/")+1:]
|
|
msg.UID = hashString(filename)
|
|
|
|
return msg, nil
|
|
}
|
|
|
|
// CEO mailboxes (Erik Svensson)
|
|
var ceoMailboxes = []string{
|
|
"erik@landvex.com",
|
|
"erik@aamos.systems",
|
|
"erik@hypbit.com",
|
|
"info@landvex.com",
|
|
"invoice@landvex.com",
|
|
"hello@quixzoom.com",
|
|
"finance@quixzoom.com",
|
|
"cfo@aamos.systems",
|
|
}
|
|
|
|
// CTO mailboxes (Johan Berglund)
|
|
var ctoMailboxes = []string{
|
|
"johan@landvex.com",
|
|
"johan@hypbit.com",
|
|
"cto@aamos.systems",
|
|
"info@aamos.systems",
|
|
"dev@hypbit.com",
|
|
}
|
|
|
|
// Shared company mailboxes
|
|
var sharedMailboxes = []string{
|
|
"recovery@landvex.com",
|
|
"social@landvex.com",
|
|
"no-reply@quixzoom.com",
|
|
"recovery@quixzoom.com",
|
|
"social@quixzoom.com",
|
|
"recovery@aamos.ai",
|
|
"recovery@apifly.com",
|
|
"recovery@corpfitt.com",
|
|
"recovery@vyra.gg",
|
|
"social@aamos.ai",
|
|
"social@apifly.com",
|
|
"social@corpfitt.com",
|
|
"social@vyra.gg",
|
|
}
|
|
|
|
// allMailboxes combines all active mailboxes
|
|
var allMailboxes = append(append(ceoMailboxes, ctoMailboxes...), sharedMailboxes...)
|
|
|
|
// getAllMessages reads messages from all mailboxes using a single docker exec
|
|
func getAllMessages(limit int) ([]DockerMailMessage, error) {
|
|
// Build find command for all mailboxes at once
|
|
var paths []string
|
|
for _, email := range allMailboxes {
|
|
maildir := getMaildirViaDocker(email)
|
|
paths = append(paths, maildir+"/cur", maildir+"/new")
|
|
}
|
|
|
|
args := append([]string{"exec", "mailu-imap-1", "find"}, paths...)
|
|
args = append(args, "-type", "f")
|
|
cmd := exec.Command("docker", args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list all maildirs: %w", err)
|
|
}
|
|
|
|
files := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
var messages []DockerMailMessage
|
|
|
|
for _, file := range files {
|
|
if file == "" {
|
|
continue
|
|
}
|
|
|
|
msg, err := readMailFileViaDocker(file)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
messages = append(messages, *msg)
|
|
if limit > 0 && len(messages) >= limit {
|
|
break
|
|
}
|
|
}
|
|
|
|
return messages, nil
|
|
}
|
|
|
|
// GetMailInboxDocker returns emails from all mailboxes
|
|
func GetMailInboxDocker(w http.ResponseWriter, r *http.Request) {
|
|
limit := 50
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
messages, err := getAllMessages(limit)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"ok": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"ok": true,
|
|
"messages": messages,
|
|
"total": len(messages),
|
|
})
|
|
}
|
|
|
|
// GetMailMessageDocker returns a single email via docker exec
|
|
func GetMailMessageDocker(w http.ResponseWriter, r *http.Request) {
|
|
uidStr := chi.URLParam(r, "uid")
|
|
uid, err := strconv.ParseUint(uidStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"invalid uid"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Search all mailboxes for message with matching UID
|
|
for _, email := range allMailboxes {
|
|
maildir := getMaildirViaDocker(email)
|
|
cmd := exec.Command("docker", "exec", "mailu-imap-1", "find", maildir+"/cur", maildir+"/new", "-type", "f")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
files := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
for _, file := range files {
|
|
if file == "" {
|
|
continue
|
|
}
|
|
|
|
filename := file[strings.LastIndex(file, "/")+1:]
|
|
if hashString(filename) == uint32(uid) {
|
|
msg, err := readMailFileViaDocker(file)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"failed to read message"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"ok": true,
|
|
"message": msg,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
http.Error(w, `{"error":"message not found"}`, http.StatusNotFound)
|
|
}
|
|
|
|
// MarkMailAsReadDocker marks a message as read via docker exec
|
|
func MarkMailAsReadDocker(w http.ResponseWriter, r *http.Request) {
|
|
uidStr := chi.URLParam(r, "uid")
|
|
uid, err := strconv.ParseUint(uidStr, 10, 32)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"invalid uid"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Search all mailboxes
|
|
for _, email := range allMailboxes {
|
|
maildir := getMaildirViaDocker(email)
|
|
cmd := exec.Command("docker", "exec", "mailu-imap-1", "find", maildir+"/new", maildir+"/cur", "-type", "f")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
files := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
for _, file := range files {
|
|
if file == "" {
|
|
continue
|
|
}
|
|
|
|
filename := file[strings.LastIndex(file, "/")+1:]
|
|
if hashString(filename) == uint32(uid) {
|
|
if strings.Contains(file, "/new/") {
|
|
newPath := file
|
|
curPath := maildir + "/cur/" + filename
|
|
|
|
moveCmd := exec.Command("docker", "exec", "mailu-imap-1", "mv", newPath, curPath)
|
|
if err := moveCmd.Run(); err != nil {
|
|
http.Error(w, `{"error":"failed to mark as read"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"ok": true,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
http.Error(w, `{"error":"message not found"}`, http.StatusNotFound)
|
|
}
|
|
|
|
// GetMailUnreadCountDocker returns unread count from all mailboxes
|
|
func GetMailUnreadCountDocker(w http.ResponseWriter, r *http.Request) {
|
|
totalCount := 0
|
|
for _, email := range allMailboxes {
|
|
maildir := getMaildirViaDocker(email)
|
|
cmd := exec.Command("docker", "exec", "mailu-imap-1", "find", maildir+"/new", "-type", "f", "2>/dev/null")
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
files := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
for _, f := range files {
|
|
if f != "" {
|
|
totalCount++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"ok": true,
|
|
"count": totalCount,
|
|
})
|
|
}
|
|
|
|
// GetMailboxes returns all configured mailboxes
|
|
func GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"ok": true,
|
|
"mailboxes": allMailboxes,
|
|
})
|
|
}
|