Files
boc/backend/handlers/mail_docker.go
T
Bernt 78b57273e2 security: Add proper authentication, RBAC, and tenant isolation
- 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
2026-08-10 12:52:48 +00:00

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,
})
}