2026-09-13 23:37:33 +00:00
|
|
|
package notification
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/smtp"
|
|
|
|
|
"os"
|
2026-09-15 08:51:19 +09:00
|
|
|
"strconv"
|
2026-09-13 23:37:33 +00:00
|
|
|
)
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
// 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.
|
2026-09-13 23:37:33 +00:00
|
|
|
type Handler struct {
|
2026-09-15 08:51:19 +09:00
|
|
|
smtpHost string
|
|
|
|
|
smtpPort string
|
|
|
|
|
smtpFrom string
|
|
|
|
|
smtpUser string
|
|
|
|
|
smtpPass string
|
|
|
|
|
gotify *GotifyClient
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
// NewHandler creates a notification handler from environment variables.
|
2026-09-13 23:37:33 +00:00
|
|
|
func NewHandler() *Handler {
|
2026-09-15 08:51:19 +09:00
|
|
|
var gotify *GotifyClient
|
|
|
|
|
gotifyURL := os.Getenv("GOTIFY_URL")
|
|
|
|
|
if gotifyURL != "" {
|
|
|
|
|
gotify = NewGotifyClient(
|
|
|
|
|
gotifyURL,
|
|
|
|
|
os.Getenv("GOTIFY_APP_TOKEN"),
|
|
|
|
|
os.Getenv("GOTIFY_CLIENT_TOKEN"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-13 23:37:33 +00:00
|
|
|
return &Handler{
|
2026-09-15 08:51:19 +09:00
|
|
|
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,
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
// ServeHTTP routes requests by X-Upstream-Path (set by dispatcher after resource matching).
|
2026-09-13 23:37:33 +00:00
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
2026-09-15 08:51:19 +09:00
|
|
|
resource := r.Header.Get("X-Resource")
|
2026-09-13 23:37:33 +00:00
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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)
|
2026-09-13 23:37:33 +00:00
|
|
|
|
|
|
|
|
default:
|
2026-09-15 08:51:19 +09:00
|
|
|
h.writeJSON(w, http.StatusNotFound, map[string]string{
|
|
|
|
|
"error": fmt.Sprintf("unknown resource: %s", resource),
|
|
|
|
|
})
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
// --- 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
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
if req.To == "" {
|
|
|
|
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'to' field"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subject := req.Subject
|
2026-09-13 23:37:33 +00:00
|
|
|
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",
|
2026-09-15 08:51:19 +09:00
|
|
|
h.smtpFrom, req.To, subject, req.Body,
|
2026-09-13 23:37:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
smtpAddr := fmt.Sprintf("%s:%s", h.smtpHost, h.smtpPort)
|
|
|
|
|
auth := smtp.PlainAuth("", h.smtpUser, h.smtpPass, h.smtpHost)
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
h.writeJSON(w, http.StatusOK, map[string]string{
|
|
|
|
|
"status": "success",
|
|
|
|
|
"messageId": fmt.Sprintf("email-%s", req.To),
|
|
|
|
|
})
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
// --- 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
|
2026-09-13 23:37:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
result, err := h.gotify.SendMessage(msg)
|
2026-09-15 02:36:02 +09:00
|
|
|
if err != nil {
|
2026-09-15 08:51:19 +09:00
|
|
|
log.Printf("error sending gotify message: %v", err)
|
|
|
|
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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)
|
2026-09-15 02:36:02 +09:00
|
|
|
if err != nil {
|
2026-09-15 08:51:19 +09:00
|
|
|
log.Printf("error creating gotify application: %v", err)
|
|
|
|
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
|
|
|
|
return
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
2026-09-15 08:51:19 +09:00
|
|
|
|
|
|
|
|
h.writeJSON(w, http.StatusCreated, result)
|
|
|
|
|
}
|
2026-09-15 02:36:02 +09:00
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
if req.ID == 0 {
|
|
|
|
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'id' field"})
|
|
|
|
|
return
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
|
|
|
|
|
2026-09-15 08:51:19 +09:00
|
|
|
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
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|
2026-09-15 08:51:19 +09:00
|
|
|
|
|
|
|
|
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)
|
2026-09-15 02:36:02 +09:00
|
|
|
}
|