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
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user