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)
This commit is contained in:
parent
16b6d9df60
commit
c5a9149220
3 changed files with 68 additions and 5 deletions
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/imc-vibe/backend/internal/db"
|
"github.com/imc-vibe/backend/internal/db"
|
||||||
|
"github.com/imc-vibe/backend/internal/mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
|
|
@ -30,6 +31,13 @@ type UpdateUserRequest struct {
|
||||||
Quota int64 `json:"quota,omitempty"`
|
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) {
|
func (h *UserHandler) List(c *gin.Context) {
|
||||||
domainName := c.Param("name")
|
domainName := c.Param("name")
|
||||||
if domainName == "" {
|
if domainName == "" {
|
||||||
|
|
@ -61,7 +69,22 @@ func (h *UserHandler) List(c *gin.Context) {
|
||||||
return
|
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) {
|
func (h *UserHandler) Get(c *gin.Context) {
|
||||||
|
|
|
||||||
40
backend/internal/mail/quota.go
Normal file
40
backend/internal/mail/quota.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,8 @@
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
domain_id: number;
|
|
||||||
quota: number;
|
quota: number;
|
||||||
used_quota: number;
|
usedQuota?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let users = $state<User[]>([]);
|
let users = $state<User[]>([]);
|
||||||
|
|
@ -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 === 0) return 'Default';
|
||||||
if (bytes < 1024) return bytes + ' B';
|
if (bytes < 1024) return bytes + ' B';
|
||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>{user.email}</td>
|
<td>{user.email}</td>
|
||||||
<td>{formatQuota(user.quota)}</td>
|
<td>{formatQuota(user.quota)}</td>
|
||||||
<td>{formatQuota(user.used_quota)}</td>
|
<td>{formatQuota(user.usedQuota)}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="danger" onclick={() => deleteUser(user.id)}>Delete</button>
|
<button class="danger" onclick={() => deleteUser(user.id)}>Delete</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue