208 lines
5.9 KiB
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()
|
|
}
|