From c5a9149220a4a398279dad3292778d83d155a012 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 23 Mar 2026 00:24:40 +0100 Subject: [PATCH] Get real mailbox size from dovecot - Add mail.GetQuota() using doveadm quota get - Fix UserWithQuota response struct - Fix frontend NaN display with Unknown fallback - Update field names to camelCase (usedQuota) --- backend/internal/api/handlers/users.go | 25 +++++++++++- backend/internal/mail/quota.go | 40 +++++++++++++++++++ .../routes/domains/[name]/users/+page.svelte | 8 ++-- 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 backend/internal/mail/quota.go diff --git a/backend/internal/api/handlers/users.go b/backend/internal/api/handlers/users.go index ddb8b79..779e309 100644 --- a/backend/internal/api/handlers/users.go +++ b/backend/internal/api/handlers/users.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/imc-vibe/backend/internal/db" + "github.com/imc-vibe/backend/internal/mail" ) type UserHandler struct { @@ -30,6 +31,13 @@ type UpdateUserRequest struct { Quota int64 `json:"quota,omitempty"` } +type UserWithQuota struct { + ID uint `json:"id"` + Email string `json:"email"` + Quota int64 `json:"quota"` + UsedQuota *int64 `json:"usedQuota"` +} + func (h *UserHandler) List(c *gin.Context) { domainName := c.Param("name") if domainName == "" { @@ -61,7 +69,22 @@ func (h *UserHandler) List(c *gin.Context) { return } - Success(c, users) + result := make([]UserWithQuota, len(users)) + for i, user := range users { + result[i] = UserWithQuota{ + ID: user.ID, + Email: user.Email, + Quota: user.Quota, + } + + quota, err := mail.GetQuota(user.Email) + if err == nil && quota != nil { + result[i].Quota = quota.Limit + result[i].UsedQuota = "a.Used + } + } + + Success(c, result) } func (h *UserHandler) Get(c *gin.Context) { diff --git a/backend/internal/mail/quota.go b/backend/internal/mail/quota.go new file mode 100644 index 0000000..4d76759 --- /dev/null +++ b/backend/internal/mail/quota.go @@ -0,0 +1,40 @@ +package mail + +import ( + "bufio" + "os/exec" + "regexp" + "strconv" + "strings" +) + +type Quota struct { + Used int64 `json:"used"` // Used storage in bytes + Limit int64 `json:"limit"` // Storage limit in bytes (-1 = unlimited) +} + +func GetQuota(email string) (*Quota, error) { + cmd := exec.Command("/usr/bin/doveadm", "quota", "get", "-u", email) + output, err := cmd.Output() + if err != nil { + return nil, err + } + + return parseDoveadmQuota(output) +} + +var quotaLineRegex = regexp.MustCompile(`STORAGE\s+(\d+)\s+(\d+)`) + +func parseDoveadmQuota(output []byte) (*Quota, error) { + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + line := scanner.Text() + matches := quotaLineRegex.FindStringSubmatch(line) + if len(matches) == 3 { + used, _ := strconv.ParseInt(matches[1], 10, 64) + limit, _ := strconv.ParseInt(matches[2], 10, 64) + return &Quota{Used: used, Limit: limit}, nil + } + } + return nil, nil +} diff --git a/frontend/src/routes/domains/[name]/users/+page.svelte b/frontend/src/routes/domains/[name]/users/+page.svelte index 32a2b93..ad0c645 100644 --- a/frontend/src/routes/domains/[name]/users/+page.svelte +++ b/frontend/src/routes/domains/[name]/users/+page.svelte @@ -4,9 +4,8 @@ interface User { id: number; email: string; - domain_id: number; quota: number; - used_quota: number; + usedQuota?: number; } let users = $state([]); @@ -85,7 +84,8 @@ } } - function formatQuota(bytes: number): string { + function formatQuota(bytes: number | undefined | null): string { + if (bytes === undefined || bytes === null || isNaN(bytes)) return 'Unknown'; if (bytes === 0) return 'Default'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; @@ -127,7 +127,7 @@ {user.email} {formatQuota(user.quota)} - {formatQuota(user.used_quota)} + {formatQuota(user.usedQuota)}