mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
233 lines
6.9 KiB
Go
233 lines
6.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
)
|
|
|
|
// PMGInstancesResponse is the canonical payload for
|
|
// `GET /api/pmg/instances`. It exposes the full Proxmox Mail Gateway
|
|
// instance snapshot — per-node cluster status with postfix queue
|
|
// detail, full mail stats (in/out, bytes, bounces, RBL/pregreet),
|
|
// quarantine breakdown by category, spam score distribution, top
|
|
// domain stats, and relay-domain configuration. The Mail Gateway
|
|
// platform-page drawer is the first consumer; the row table keeps
|
|
// reading the slim ResourcePMGMeta projection through /api/resources.
|
|
type PMGInstancesResponse struct {
|
|
Data []models.PMGInstance `json:"data"`
|
|
Meta PMGInstancesMeta `json:"meta"`
|
|
}
|
|
|
|
type PMGInstancesMeta struct {
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
func (r *Router) handleListPMGInstances(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed",
|
|
"Only GET method is allowed", nil)
|
|
return
|
|
}
|
|
|
|
monitor := r.getTenantMonitor(req.Context())
|
|
if monitor == nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "no_monitor",
|
|
"Monitor not available", nil)
|
|
return
|
|
}
|
|
|
|
readState := monitor.GetUnifiedReadStateOrSnapshot()
|
|
views := readState.PMGInstances()
|
|
|
|
mappedInstances := make([]models.PMGInstance, len(views))
|
|
for i, v := range views {
|
|
mappedInstances[i] = mapPMGInstanceViewToModel(v)
|
|
}
|
|
|
|
instances := filterPMGInstances(mappedInstances, req)
|
|
|
|
response := PMGInstancesResponse{
|
|
Data: instances,
|
|
Meta: PMGInstancesMeta{Total: len(instances)},
|
|
}
|
|
|
|
if err := utils.WriteJSONResponse(w, response); err != nil {
|
|
log.Error().Err(err).Msg("Failed to encode PMG instances response")
|
|
writeErrorResponse(w, http.StatusInternalServerError, "encoding_error",
|
|
"Failed to encode PMG instances", nil)
|
|
}
|
|
}
|
|
|
|
// mapPMGInstanceViewToModel projects the canonical PMG read-state view onto
|
|
// the legacy API payload consumed by existing PMG surfaces.
|
|
func mapPMGInstanceViewToModel(v *unifiedresources.PMGInstanceView) models.PMGInstance {
|
|
if v == nil {
|
|
return models.PMGInstance{}
|
|
}
|
|
|
|
nodes := v.Nodes()
|
|
modelNodes := make([]models.PMGNodeStatus, len(nodes))
|
|
for i, n := range nodes {
|
|
modelNodes[i] = models.PMGNodeStatus{
|
|
Name: n.Name,
|
|
Status: n.Status,
|
|
Role: n.Role,
|
|
Uptime: n.Uptime,
|
|
LoadAvg: n.LoadAvg,
|
|
}
|
|
if n.QueueStatus != nil {
|
|
modelNodes[i].QueueStatus = &models.PMGQueueStatus{
|
|
Active: n.QueueStatus.Active,
|
|
Deferred: n.QueueStatus.Deferred,
|
|
Hold: n.QueueStatus.Hold,
|
|
Incoming: n.QueueStatus.Incoming,
|
|
Total: n.QueueStatus.Total,
|
|
OldestAge: n.QueueStatus.OldestAge,
|
|
UpdatedAt: n.QueueStatus.UpdatedAt,
|
|
}
|
|
}
|
|
}
|
|
|
|
var modelMailStats *models.PMGMailStats
|
|
if stats := v.MailStats(); stats != nil {
|
|
countTotal := stats.CountTotal
|
|
if countTotal == 0 {
|
|
countTotal = v.MailCountTotal()
|
|
}
|
|
updatedAt := stats.UpdatedAt
|
|
if updatedAt.IsZero() {
|
|
updatedAt = v.LastUpdated()
|
|
}
|
|
modelMailStats = &models.PMGMailStats{
|
|
Timeframe: stats.Timeframe,
|
|
CountTotal: countTotal,
|
|
CountIn: stats.CountIn,
|
|
CountOut: stats.CountOut,
|
|
SpamIn: stats.SpamIn,
|
|
SpamOut: stats.SpamOut,
|
|
VirusIn: stats.VirusIn,
|
|
VirusOut: stats.VirusOut,
|
|
BouncesIn: stats.BouncesIn,
|
|
BouncesOut: stats.BouncesOut,
|
|
BytesIn: stats.BytesIn,
|
|
BytesOut: stats.BytesOut,
|
|
GreylistCount: stats.GreylistCount,
|
|
JunkIn: stats.JunkIn,
|
|
AverageProcessTimeMs: stats.AverageProcessTimeMs,
|
|
RBLRejects: stats.RBLRejects,
|
|
PregreetRejects: stats.PregreetRejects,
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
}
|
|
|
|
var modelQuarantine *models.PMGQuarantineTotals
|
|
if q := v.Quarantine(); q != nil {
|
|
modelQuarantine = &models.PMGQuarantineTotals{
|
|
Spam: q.Spam,
|
|
Virus: q.Virus,
|
|
Attachment: q.Attachment,
|
|
Blacklisted: q.Blacklisted,
|
|
}
|
|
}
|
|
|
|
spamDist := v.SpamDistribution()
|
|
modelSpamDist := make([]models.PMGSpamBucket, len(spamDist))
|
|
for i, b := range spamDist {
|
|
modelSpamDist[i] = models.PMGSpamBucket{
|
|
Score: b.Bucket,
|
|
Count: b.Count,
|
|
}
|
|
}
|
|
|
|
relayDomains := v.RelayDomains()
|
|
modelRelayDomains := make([]models.PMGRelayDomain, len(relayDomains))
|
|
for i, d := range relayDomains {
|
|
modelRelayDomains[i] = models.PMGRelayDomain{
|
|
Domain: d.Domain,
|
|
Comment: d.Comment,
|
|
}
|
|
}
|
|
|
|
domainStats := v.DomainStats()
|
|
modelDomainStats := make([]models.PMGDomainStat, len(domainStats))
|
|
for i, s := range domainStats {
|
|
modelDomainStats[i] = models.PMGDomainStat{
|
|
Domain: s.Domain,
|
|
MailCount: s.MailCount,
|
|
SpamCount: s.SpamCount,
|
|
VirusCount: s.VirusCount,
|
|
Bytes: s.Bytes,
|
|
}
|
|
}
|
|
|
|
host := v.HostURL()
|
|
if strings.TrimSpace(host) == "" {
|
|
host = v.Hostname()
|
|
}
|
|
guestURL := v.GuestURL()
|
|
if strings.TrimSpace(guestURL) == "" {
|
|
guestURL = v.CustomURL()
|
|
}
|
|
instanceID := v.InstanceID()
|
|
if strings.TrimSpace(instanceID) == "" {
|
|
instanceID = v.ID()
|
|
}
|
|
|
|
inst := models.PMGInstance{
|
|
ID: instanceID,
|
|
Name: v.Name(),
|
|
Host: host,
|
|
GuestURL: guestURL,
|
|
Status: string(v.Status()),
|
|
Version: v.Version(),
|
|
Nodes: modelNodes,
|
|
MailStats: modelMailStats,
|
|
MailCount: v.MailCount(),
|
|
SpamDistribution: modelSpamDist,
|
|
Quarantine: modelQuarantine,
|
|
RelayDomains: modelRelayDomains,
|
|
DomainStats: modelDomainStats,
|
|
DomainStatsAsOf: v.DomainStatsAsOf(),
|
|
ConnectionHealth: v.ConnectionHealth(),
|
|
LastSeen: v.LastSeen(),
|
|
LastUpdated: v.LastUpdated(),
|
|
}
|
|
return inst.NormalizeCollections()
|
|
}
|
|
|
|
// filterPMGInstances narrows the snapshot by optional `id=` or
|
|
// `name=` query parameters. With neither, returns the full list.
|
|
func filterPMGInstances(instances []models.PMGInstance, req *http.Request) []models.PMGInstance {
|
|
query := req.URL.Query()
|
|
idFilter := strings.TrimSpace(query.Get("id"))
|
|
nameFilter := strings.TrimSpace(query.Get("name"))
|
|
|
|
if idFilter == "" && nameFilter == "" {
|
|
out := make([]models.PMGInstance, len(instances))
|
|
copy(out, instances)
|
|
return out
|
|
}
|
|
|
|
out := make([]models.PMGInstance, 0, len(instances))
|
|
for _, inst := range instances {
|
|
if idFilter != "" && !strings.EqualFold(strings.TrimSpace(inst.ID), idFilter) {
|
|
continue
|
|
}
|
|
if nameFilter != "" && !strings.EqualFold(strings.TrimSpace(inst.Name), nameFilter) {
|
|
continue
|
|
}
|
|
out = append(out, inst)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Router) registerPMGRoutes() {
|
|
r.mux.HandleFunc("/api/pmg/instances", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleListPMGInstances)))
|
|
}
|