package notification import ( "encoding/json" "fmt" "log" "net/http" "net/smtp" "os" "strconv" ) // Handler handles notification requests routed via X-Resource header. // Supports: send-email, send-gotify, list-messages, delete-message, // delete-all-messages, list-applications, create-application, delete-application. type Handler struct { smtpHost string smtpPort string smtpFrom string smtpUser string smtpPass string gotify *GotifyClient } // NewHandler creates a notification handler from environment variables. func NewHandler() *Handler { var gotify *GotifyClient gotifyURL := os.Getenv("GOTIFY_URL") if gotifyURL != "" { gotify = NewGotifyClient( gotifyURL, os.Getenv("GOTIFY_APP_TOKEN"), os.Getenv("GOTIFY_CLIENT_TOKEN"), ) } return &Handler{ smtpHost: os.Getenv("SMTP_HOST"), smtpPort: os.Getenv("SMTP_PORT"), smtpFrom: os.Getenv("SMTP_FROM"), smtpUser: os.Getenv("SMTP_USER"), smtpPass: os.Getenv("SMTP_PASS"), gotify: gotify, } } // ServeHTTP routes requests by X-Upstream-Path (set by dispatcher after resource matching). func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { resource := r.Header.Get("X-Resource") switch resource { // --- Email --- case "send-email": h.handleSendEmail(w, r) // --- Gotify Messages --- case "send-message": h.handleSendGotify(w, r) case "list-messages": h.handleListMessages(w, r) case "delete-message": h.handleDeleteMessage(w, r) case "delete-all-messages": h.handleDeleteAllMessages(w, r) // --- Gotify Applications --- case "list-applications": h.handleListApplications(w, r) case "create-application": h.handleCreateApplication(w, r) case "delete-application": h.handleDeleteApplication(w, r) default: h.writeJSON(w, http.StatusNotFound, map[string]string{ "error": fmt.Sprintf("unknown resource: %s", resource), }) } } // --- Email --- type SendEmailRequest struct { To string `json:"to"` CC string `json:"cc,omitempty"` Subject string `json:"subject"` Body string `json:"body"` } func (h *Handler) handleSendEmail(w http.ResponseWriter, r *http.Request) { var req SendEmailRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()}) return } if req.To == "" { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'to' field"}) return } subject := req.Subject if subject == "" { subject = "Notification" } msg := fmt.Sprintf( "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", h.smtpFrom, req.To, subject, req.Body, ) smtpAddr := fmt.Sprintf("%s:%s", h.smtpHost, h.smtpPort) auth := smtp.PlainAuth("", h.smtpUser, h.smtpPass, h.smtpHost) if err := smtp.SendMail(smtpAddr, auth, h.smtpFrom, []string{req.To}, []byte(msg)); err != nil { log.Printf("error sending email to %s: %v", req.To, err) h.writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to send email: " + err.Error()}) return } h.writeJSON(w, http.StatusOK, map[string]string{ "status": "success", "messageId": fmt.Sprintf("email-%s", req.To), }) } // --- Gotify Messages --- func (h *Handler) handleSendGotify(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } var msg GotifyMessage if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()}) return } result, err := h.gotify.SendMessage(msg) if err != nil { log.Printf("error sending gotify message: %v", err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusOK, result) } func (h *Handler) handleListMessages(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } limit := 50 if l := r.URL.Query().Get("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed } } result, err := h.gotify.ListMessages(limit) if err != nil { log.Printf("error listing gotify messages: %v", err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusOK, result) } func (h *Handler) handleDeleteMessage(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } var req struct { ID int `json:"id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()}) return } if req.ID == 0 { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'id' field"}) return } if err := h.gotify.DeleteMessage(req.ID); err != nil { log.Printf("error deleting gotify message %d: %v", req.ID, err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func (h *Handler) handleDeleteAllMessages(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } if err := h.gotify.DeleteAllMessages(); err != nil { log.Printf("error deleting all gotify messages: %v", err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusOK, map[string]string{"status": "all messages deleted"}) } // --- Gotify Applications --- func (h *Handler) handleListApplications(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } result, err := h.gotify.ListApplications() if err != nil { log.Printf("error listing gotify applications: %v", err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusOK, result) } func (h *Handler) handleCreateApplication(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } var app GotifyApplication if err := json.NewDecoder(r.Body).Decode(&app); err != nil { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()}) return } result, err := h.gotify.CreateApplication(app) if err != nil { log.Printf("error creating gotify application: %v", err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusCreated, result) } func (h *Handler) handleDeleteApplication(w http.ResponseWriter, r *http.Request) { if h.gotify == nil { h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"}) return } var req struct { ID int `json:"id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()}) return } if req.ID == 0 { h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'id' field"}) return } if err := h.gotify.DeleteApplication(req.ID); err != nil { log.Printf("error deleting gotify application %d: %v", req.ID, err) h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } h.writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } // --- Helpers --- func (h *Handler) writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) }