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