benzcloud-plugin-mail/main.go

208 lines
5.9 KiB
Go

package main
import (
"embed"
"encoding/json"
"flag"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"github.com/benzjeremy/benzcloud-plugin-mail/internal/mailstore"
"github.com/benzjeremy/benzcloud-plugin-mail/internal/protocols"
)
//go:embed web/*
var webFS embed.FS
type SendRequest struct {
From string `json:"from"`
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
}
func main() {
httpPort := flag.Int("port", 8092, "HTTP webmail & API port")
smtpPort := flag.Int("smtp", 1025, "SMTP server port")
imapPort := flag.Int("imap", 1143, "IMAP server port")
dataDir := flag.String("data", "./data", "Directory for mailboxes")
baseDomain := flag.String("domain", "benzcloud.local", "Base domain for mail")
flag.Parse()
log.Printf("[BenzCloud Mail] Initializing Mail Plugin v1.0 on domain: %s\n", *baseDomain)
store, err := mailstore.NewStore(*dataDir, *baseDomain)
if err != nil {
log.Fatalf("Failed to initialize mailstore: %v", err)
}
// Seed welcome email for admin if empty
existing := store.GetMessages("admin", "ALL")
if len(existing) == 0 {
_, _ = store.Deliver(
"system@"+*baseDomain,
"admin@"+*baseDomain,
"Welcome to BenzCloud Mail!",
"Welcome to your private, self-hosted BenzCloud Mail service!\n\n"+
"Features:\n"+
"- Webmail UI with Inbox, Sent, and Trash\n"+
"- Full SMTP (port "+fmt.Sprintf("%d", *smtpPort)+") and IMAP (port "+fmt.Sprintf("%d", *imapPort)+") support\n"+
"- Out-of-the-box support for Thunderbird, K-9 Mail, and Apple Mail\n"+
"- 100% private and decentralized within your Mesh VPN\n\n"+
"Enjoy complete digital sovereignty!",
)
}
// Launch SMTP server
smtpServer := protocols.NewSMTPServer(*smtpPort, store)
if err := smtpServer.Start(); err != nil {
log.Printf("[BenzCloud Mail] Warning: SMTP server failed on port %d: %v", *smtpPort, err)
}
defer smtpServer.Stop()
// Launch IMAP server
imapServer := protocols.NewIMAPServer(*imapPort, store)
if err := imapServer.Start(); err != nil {
log.Printf("[BenzCloud Mail] Warning: IMAP server failed on port %d: %v", *imapPort, err)
}
defer imapServer.Stop()
// HTTP Router
mux := http.NewServeMux()
// Health endpoint for BenzCloud Supervisor / Router
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"plugin": "mail",
"version": "v1.0",
"subdomain": "mail",
"smtp_port": *smtpPort,
"imap_port": *imapPort,
})
})
// API: Users list
mux.HandleFunc("/api/mail/users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
users := []string{"admin"}
_ = json.NewEncoder(w).Encode(users)
})
// API: Get messages
mux.HandleFunc("/api/mail/messages", func(w http.ResponseWriter, r *http.Request) {
user := r.URL.Query().Get("user")
if user == "" {
user = "admin"
}
folder := r.URL.Query().Get("folder")
if folder == "" {
folder = "INBOX"
}
msgs := store.GetMessages(user, folder)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(msgs)
})
// API: Send message
mux.HandleFunc("/api/mail/send", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req SendRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.To == "" {
http.Error(w, "Recipient is required", http.StatusBadRequest)
return
}
msg, err := store.Deliver(req.From, req.To, req.Subject, req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(msg)
})
// API: Mark as read
mux.HandleFunc("/api/mail/read", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
user := r.URL.Query().Get("user")
id := r.URL.Query().Get("id")
if user == "" || id == "" {
http.Error(w, "Missing user or id", http.StatusBadRequest)
return
}
if err := store.MarkAsRead(user, id); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
})
// API: Delete message
mux.HandleFunc("/api/mail/delete", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
user := r.URL.Query().Get("user")
id := r.URL.Query().Get("id")
if user == "" || id == "" {
http.Error(w, "Missing user or id", http.StatusBadRequest)
return
}
if err := store.DeleteMessage(user, id); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
})
// Static Webmail Files
subWeb, err := fs.Sub(webFS, "web")
if err != nil {
log.Fatalf("Failed to create sub filesystem: %v", err)
}
fileServer := http.FileServer(http.FS(subWeb))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/health" {
fileServer.ServeHTTP(w, r)
return
}
})
server := &http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", *httpPort),
Handler: mux,
}
go func() {
log.Printf("[BenzCloud Mail] Webmail UI & API listening on http://0.0.0.0:%d\n", *httpPort)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Println("[BenzCloud Mail] Shutting down gracefully...")
_ = server.Close()
}