This commit is contained in:
2026-06-01 19:51:19 +08:00
parent 4e233c82b4
commit c3be042acb
6 changed files with 140 additions and 43 deletions
+59 -4
View File
@@ -9,6 +9,7 @@ import (
"mail_go/internal/db"
"mail_go/internal/dkim"
"mail_go/internal/storage"
"mail_go/internal/store"
"github.com/gin-gonic/gin"
@@ -17,12 +18,13 @@ import (
// AdminHandler handles admin-related routes (dashboard, domain/user management).
type AdminHandler struct {
stores *store.Stores
stores *store.Stores
storage *storage.AttachmentStorage
}
// NewAdminHandler creates a new AdminHandler with the given stores.
func NewAdminHandler(stores *store.Stores) *AdminHandler {
return &AdminHandler{stores: stores}
// NewAdminHandler creates a new AdminHandler with the given stores and attachment storage.
func NewAdminHandler(stores *store.Stores, attStorage *storage.AttachmentStorage) *AdminHandler {
return &AdminHandler{stores: stores, storage: attStorage}
}
// Dashboard renders the admin dashboard with summary statistics.
@@ -630,6 +632,59 @@ func (h *AdminHandler) ListMails(c *gin.Context) {
})
}
// AdminViewMail renders the detail view of a specific mail for admin.
func (h *AdminHandler) AdminViewMail(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的邮件ID")
return
}
msg, err := h.stores.Mails.GetByID(uint(id))
if err != nil {
c.String(http.StatusNotFound, "邮件不存在")
return
}
// 加载附件
attachments, _ := h.stores.Attachments.ListByMessage(uint(id))
currentUser, _ := c.Get("currentUser")
c.HTML(200, "admin_mail_view", gin.H{
"currentUser": currentUser,
"message": msg,
"attachments": attachments,
"activeFolder": "mails",
})
}
// AdminDownloadAttachment serves an attachment file for admin (bypasses user ownership check).
func (h *AdminHandler) AdminDownloadAttachment(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的附件ID")
return
}
att, err := h.stores.Attachments.GetByID(uint(id))
if err != nil {
c.String(http.StatusNotFound, "附件不存在")
return
}
data, err := h.storage.Read(att.FilePath)
if err != nil {
c.String(http.StatusInternalServerError, "读取附件失败")
return
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", att.FileName))
c.Data(http.StatusOK, att.ContentType, data)
}
// formIntOrDefault extracts an integer from a form field, returning the default if missing/invalid.
// formIntOrDefault extracts an integer from a form field, returning the default if missing/invalid.
func formIntOrDefault(c *gin.Context, key string, defaultVal int) int {
val := c.PostForm(key)
+3 -1
View File
@@ -112,7 +112,7 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
func (ws *WebServer) registerRoutes() {
authHandler := handlers.NewAuthHandler(ws.stores, ws.authCfg, ws.banCfg)
mailHandler := handlers.NewMailHandler(ws.stores, ws.storage)
adminHandler := handlers.NewAdminHandler(ws.stores)
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage)
// Apply BanMiddleware globally before public routes
ws.engine.Use(middleware.BanMiddleware(ws.stores))
@@ -170,6 +170,8 @@ func (ws *WebServer) registerRoutes() {
admin.GET("/users/:id/edit", adminHandler.EditUser)
admin.POST("/users/:id", adminHandler.UpdateUser)
admin.GET("/mails", adminHandler.ListMails)
admin.GET("/mails/:id", adminHandler.AdminViewMail)
admin.GET("/attachment/:id", adminHandler.AdminDownloadAttachment)
admin.GET("/bans", adminHandler.ListBans)
admin.POST("/bans/:id/unban", adminHandler.UnbanIP)
admin.POST("/bans/cleanup", adminHandler.CleanupBans)
+10 -1
View File
@@ -46,10 +46,19 @@
</div>
<div class="form-group">
<label>
<input type="checkbox" name="tls_enabled" {{if .domain.TlsEnabled}}checked{{end}}>
<input type="checkbox" name="tls_enabled" id="tls_enabled" {{if .domain.TlsEnabled}}checked{{end}} onchange="togglePorts()">
启用 TLS
</label>
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">勾选后端口将自动切换为 SSL/TLS 标准端口</p>
</div>
<script>
function togglePorts() {
var tls = document.getElementById('tls_enabled').checked;
document.querySelector('input[name="smtp_port"]').value = tls ? 465 : 25;
document.querySelector('input[name="imap_port"]').value = tls ? 993 : 143;
document.querySelector('input[name="pop3_port"]').value = tls ? 995 : 110;
}
</script>
{{if .isEdit}}
<div class="form-group">
<label>
@@ -0,0 +1,60 @@
{{define "admin_mail_view"}}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看邮件 - MailGo 管理后台</title>
{{template "styles" .}}
</head>
<body>
{{template "navbar" .}}
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">返回邮箱</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
<a href="/admin/mails" class="active">所有邮件</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
</div>
<div class="content">
<div class="card">
<div style="margin-bottom:16px;">
<a href="/admin/mails" class="btn" style="background:#bdc3c7;color:#fff;">返回邮件列表</a>
</div>
<h2>{{if .message.Subject}}{{.message.Subject}}{{else}}(无主题){{end}}</h2>
<div class="mail-meta" style="margin-top:12px;">
<p><strong>发件人:</strong> {{.message.FromAddr}}</p>
<p><strong>收件人:</strong> {{.message.ToAddr}}</p>
{{if .message.CcAddr}}<p><strong>抄送:</strong> {{.message.CcAddr}}</p>{{end}}
<p><strong>所属用户:</strong> {{if .message.User.ID}}{{.message.User.Username}}{{else}}—{{end}}</p>
<p><strong>文件夹:</strong> {{.message.Folder}}</p>
<p><strong>时间:</strong> {{.message.Date.Format "2006-01-02 15:04:05"}}</p>
</div>
<div class="mail-body">
{{if .message.HtmlBody}}
{{.message.HtmlBody | safeHTML}}
{{else}}
<pre style="white-space:pre-wrap;font-family:inherit;">{{.message.TextBody}}</pre>
{{end}}
</div>
{{if .attachments}}
<div class="attachment-list">
<h4 style="margin-bottom:8px;">附件</h4>
{{range .attachments}}
<div class="attachment-item">
📎 <a href="/admin/attachment/{{.ID}}">{{.FileName}}</a>
<span style="color:#7f8c8d;font-size:12px;">({{formatBytes .FileSize}})</span>
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
</div>
</body>
</html>
{{end}}
+1 -1
View File
@@ -45,7 +45,7 @@
</thead>
<tbody>
{{range .messages}}
<tr>
<tr style="cursor:pointer;" onclick="location.href='/admin/mails/{{.ID}}'">
<td>{{.FromAddr}}</td>
<td>{{.ToAddr}}</td>
<td>{{.Subject}}</td>