一阶段ok

This commit is contained in:
2026-06-01 18:59:55 +08:00
commit 9e50d05e71
52 changed files with 6155 additions and 0 deletions
+435
View File
@@ -0,0 +1,435 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"mail_go/internal/db"
"mail_go/internal/store"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
// AdminHandler handles admin-related routes (dashboard, domain/user management).
type AdminHandler struct {
stores *store.Stores
}
// NewAdminHandler creates a new AdminHandler with the given stores.
func NewAdminHandler(stores *store.Stores) *AdminHandler {
return &AdminHandler{stores: stores}
}
// Dashboard renders the admin dashboard with summary statistics.
func (h *AdminHandler) Dashboard(c *gin.Context) {
_, domainCount, _ := h.stores.Domains.List(1, 1)
_, userCount, _ := h.stores.Users.ListAll(1, 1)
currentUser, _ := c.Get("currentUser")
c.HTML(200, "admin_dashboard", gin.H{
"currentUser": currentUser,
"domainCount": domainCount,
"userCount": userCount,
"activeFolder": "admin",
})
}
// ListDomains renders the domain list page.
func (h *AdminHandler) ListDomains(c *gin.Context) {
page := getPageParam(c, "page", 1)
domains, total, err := h.stores.Domains.List(page, 20)
if err != nil {
c.String(http.StatusInternalServerError, "加载域名列表失败: %v", err)
return
}
currentUser, _ := c.Get("currentUser")
totalPages := int(total) / 20
if int(total)%20 > 0 {
totalPages++
}
if totalPages < 1 {
totalPages = 0
}
c.HTML(200, "admin_domains", gin.H{
"currentUser": currentUser,
"domains": domains,
"total": total,
"page": page,
"pageSize": 20,
"totalPages": totalPages,
"activeFolder": "admin",
})
}
// NewDomain renders the new domain form page.
func (h *AdminHandler) NewDomain(c *gin.Context) {
currentUser, _ := c.Get("currentUser")
c.HTML(200, "admin_domain_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "",
"isEdit": false,
"domain": &db.Domain{},
})
}
// CreateDomain processes the new domain form submission.
func (h *AdminHandler) CreateDomain(c *gin.Context) {
name := c.PostForm("name")
smtpPort := formIntOrDefault(c, "smtp_port", 25)
imapPort := formIntOrDefault(c, "imap_port", 143)
pop3Port := formIntOrDefault(c, "pop3_port", 110)
tlsEnabled := c.PostForm("tls_enabled") == "on"
if name == "" {
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusBadRequest, "admin_domain_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "请输入域名",
"isEdit": false,
"domain": &db.Domain{
Name: name,
SmtpPort: smtpPort,
ImapPort: imapPort,
Pop3Port: pop3Port,
TlsEnabled: tlsEnabled,
},
})
return
}
domain := &db.Domain{
Name: name,
SmtpPort: smtpPort,
ImapPort: imapPort,
Pop3Port: pop3Port,
TlsEnabled: tlsEnabled,
}
if err := h.stores.Domains.Create(domain); err != nil {
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusBadRequest, "admin_domain_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": fmt.Sprintf("创建域名失败: %v", err),
"isEdit": false,
"domain": domain,
})
return
}
c.Redirect(http.StatusFound, "/admin/domains")
}
// DeleteDomain removes a domain by ID.
func (h *AdminHandler) DeleteDomain(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的域名ID")
return
}
if err := h.stores.Domains.Delete(uint(id)); err != nil {
c.String(http.StatusInternalServerError, "删除域名失败: %v", err)
return
}
c.Redirect(http.StatusFound, "/admin/domains")
}
// DNSHint renders the DNS configuration hints for a specific domain.
func (h *AdminHandler) DNSHint(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的域名ID")
return
}
domain, err := h.stores.Domains.GetByID(uint(id))
if err != nil {
c.String(http.StatusNotFound, "域名不存在")
return
}
currentUser, _ := c.Get("currentUser")
c.HTML(200, "admin_dns_hint", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"domain": domain,
})
}
// ListUsers renders the user list page.
func (h *AdminHandler) ListUsers(c *gin.Context) {
page := getPageParam(c, "page", 1)
users, total, err := h.stores.Users.ListAll(page, 20)
if err != nil {
c.String(http.StatusInternalServerError, "加载用户列表失败: %v", err)
return
}
// Get all domains for display
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
totalPages := int(total) / 20
if int(total)%20 > 0 {
totalPages++
}
if totalPages < 1 {
totalPages = 0
}
c.HTML(200, "admin_users", gin.H{
"currentUser": currentUser,
"users": users,
"domains": domains,
"total": total,
"page": page,
"pageSize": 20,
"totalPages": totalPages,
"activeFolder": "admin",
})
}
// NewUser renders the new user form page.
func (h *AdminHandler) NewUser(c *gin.Context) {
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(200, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "",
"isEdit": false,
"domains": domains,
"user": &db.User{},
})
}
// CreateUser processes the new user form submission.
func (h *AdminHandler) CreateUser(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
domainID := formUintOrDefault(c, "domain_id", 0)
quotaGB := formIntOrDefault(c, "quota_gb", 5)
isAdmin := c.PostForm("is_admin") == "on"
if username == "" || password == "" || domainID == 0 {
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusBadRequest, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "请填写所有必填字段",
"isEdit": false,
"domains": domains,
"user": &db.User{
Username: username,
DomainID: domainID,
IsAdmin: isAdmin,
},
})
return
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusInternalServerError, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "密码加密失败",
"isEdit": false,
"domains": domains,
"user": &db.User{
Username: username,
DomainID: domainID,
IsAdmin: isAdmin,
},
})
return
}
quotaBytes := int64(quotaGB) * 1024 * 1024 * 1024
user := &db.User{
Username: username,
PasswordHash: string(hashedPassword),
DomainID: domainID,
QuotaBytes: quotaBytes,
UsedBytes: 0,
IsActive: true,
IsAdmin: isAdmin,
}
if err := h.stores.Users.Create(user); err != nil {
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusBadRequest, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": fmt.Sprintf("创建用户失败: %v", err),
"isEdit": false,
"domains": domains,
"user": user,
})
return
}
c.Redirect(http.StatusFound, "/admin/users")
}
// DeleteUser removes a user by ID.
func (h *AdminHandler) DeleteUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的用户ID")
return
}
currentUser, _ := c.Get("currentUser")
if currentUser.(*db.User).ID == uint(id) {
c.String(http.StatusBadRequest, "不能删除自己的账户")
return
}
if err := h.stores.Users.Delete(uint(id)); err != nil {
c.String(http.StatusInternalServerError, "删除用户失败: %v", err)
return
}
c.Redirect(http.StatusFound, "/admin/users")
}
// EditUser renders the edit user form page.
func (h *AdminHandler) EditUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的用户ID")
return
}
user, err := h.stores.Users.GetByID(uint(id))
if err != nil {
c.String(http.StatusNotFound, "用户不存在")
return
}
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(200, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "",
"isEdit": true,
"domains": domains,
"user": user,
})
}
// UpdateUser processes the edit user form submission.
func (h *AdminHandler) UpdateUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "无效的用户ID")
return
}
user, err := h.stores.Users.GetByID(uint(id))
if err != nil {
c.String(http.StatusNotFound, "用户不存在")
return
}
username := c.PostForm("username")
domainID := formUintOrDefault(c, "domain_id", user.DomainID)
quotaGB := formIntOrDefault(c, "quota_gb", int(user.QuotaBytes/(1024*1024*1024)))
isActive := c.PostForm("is_active") == "on"
isAdmin := c.PostForm("is_admin") == "on"
password := c.PostForm("password")
if username != "" {
user.Username = username
}
user.DomainID = domainID
user.QuotaBytes = int64(quotaGB) * 1024 * 1024 * 1024
user.IsActive = isActive
user.IsAdmin = isAdmin
// Update password only if a new one is provided
if password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusInternalServerError, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": "密码加密失败",
"isEdit": true,
"domains": domains,
"user": user,
})
return
}
user.PasswordHash = string(hashedPassword)
}
if err := h.stores.Users.Update(user); err != nil {
domains, _, _ := h.stores.Domains.List(1, 1000)
currentUser, _ := c.Get("currentUser")
c.HTML(http.StatusBadRequest, "admin_user_form", gin.H{
"currentUser": currentUser,
"activeFolder": "admin",
"error": fmt.Sprintf("更新用户失败: %v", err),
"isEdit": true,
"domains": domains,
"user": user,
})
return
}
c.Redirect(http.StatusFound, "/admin/users")
}
// 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)
if val == "" {
return defaultVal
}
n, err := strconv.Atoi(val)
if err != nil {
return defaultVal
}
return n
}
// formUintOrDefault extracts a uint from a form field, returning the default if missing/invalid.
func formUintOrDefault(c *gin.Context, key string, defaultVal uint) uint {
val := c.PostForm(key)
if val == "" {
return defaultVal
}
n, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return defaultVal
}
return uint(n)
}
+76
View File
@@ -0,0 +1,76 @@
package handlers
import (
"mail_go/internal/store"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// AuthHandler handles authentication-related routes (login, logout).
type AuthHandler struct {
stores *store.Stores
}
// NewAuthHandler creates a new AuthHandler with the given stores.
func NewAuthHandler(stores *store.Stores) *AuthHandler {
return &AuthHandler{stores: stores}
}
// ShowLogin renders the login page.
func (h *AuthHandler) ShowLogin(c *gin.Context) {
// If already logged in, redirect to inbox
session := sessions.Default(c)
if session.Get("userID") != nil {
c.Redirect(302, "/inbox")
return
}
c.HTML(200, "login", gin.H{
"error": "",
})
}
// DoLogin processes the login form submission.
// It authenticates the user with email and password, sets session data
// on success, or re-renders the login page with an error on failure.
func (h *AuthHandler) DoLogin(c *gin.Context) {
email := c.PostForm("email")
password := c.PostForm("password")
if email == "" || password == "" {
c.HTML(200, "login", gin.H{
"error": "请输入邮箱和密码",
})
return
}
user, err := h.stores.Users.Authenticate(email, password)
if err != nil {
c.HTML(200, "login", gin.H{
"error": "用户名或密码错误",
})
return
}
// Set session values
session := sessions.Default(c)
session.Set("userID", user.ID)
session.Set("userEmail", user.Username+"@"+user.Domain.Name)
session.Set("isAdmin", user.IsAdmin)
if err := session.Save(); err != nil {
c.HTML(200, "login", gin.H{
"error": "会话保存失败,请重试",
})
return
}
c.Redirect(302, "/inbox")
}
// DoLogout clears the session and redirects to the login page.
func (h *AuthHandler) DoLogout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save()
c.Redirect(302, "/login")
}
+521
View File
@@ -0,0 +1,521 @@
package handlers
import (
"fmt"
"io"
"net/http"
"net/smtp"
"path/filepath"
"strconv"
"strings"
"time"
"mail_go/internal/db"
"mail_go/internal/storage"
"mail_go/internal/store"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
// MailHandler handles mail-related routes (inbox, compose, sent, view, etc.).
type MailHandler struct {
stores *store.Stores
storage *storage.AttachmentStorage
}
// NewMailHandler creates a new MailHandler with the given stores and attachment storage.
func NewMailHandler(stores *store.Stores, attStorage *storage.AttachmentStorage) *MailHandler {
return &MailHandler{stores: stores, storage: attStorage}
}
// Inbox renders the inbox page showing all messages in the user's INBOX folder.
func (h *MailHandler) Inbox(c *gin.Context) {
userID := c.GetUint("userID")
page := getPageParam(c, "page", 1)
messages, total, err := h.stores.Mails.ListByUserAndFolder(userID, "INBOX", page, 20)
if err != nil {
c.String(http.StatusInternalServerError, "加载收件箱失败: %v", err)
return
}
unreadCount, _ := h.stores.Mails.CountUnread(userID, "INBOX")
currentUser, _ := c.Get("currentUser")
totalPages := int(total) / 20
if int(total)%20 > 0 {
totalPages++
}
if totalPages < 1 {
totalPages = 0
}
c.HTML(200, "inbox", gin.H{
"currentUser": currentUser,
"messages": messages,
"total": total,
"unreadCount": unreadCount,
"page": page,
"pageSize": 20,
"totalPages": totalPages,
"folder": "INBOX",
"activeFolder": "inbox",
})
}
// View renders the email detail page for a specific message.
func (h *MailHandler) View(c *gin.Context) {
userID := c.GetUint("userID")
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
}
// Verify the message belongs to the current user
if msg.UserID != userID {
c.String(http.StatusForbidden, "禁止访问")
return
}
// Load attachments
attachments, _ := h.stores.Attachments.ListByMessage(uint(id))
// Auto mark as read
if !msg.IsRead {
_ = h.stores.Mails.MarkRead(uint(id))
msg.IsRead = true
}
currentUser, _ := c.Get("currentUser")
c.HTML(200, "view", gin.H{
"currentUser": currentUser,
"message": msg,
"attachments": attachments,
"activeFolder": resolveActiveFolder(msg.Folder),
})
}
// Compose renders the email composition page.
func (h *MailHandler) Compose(c *gin.Context) {
currentUser, _ := c.Get("currentUser")
c.HTML(200, "compose", gin.H{
"currentUser": currentUser,
"activeFolder": "compose",
"error": "",
"to": c.Query("to"),
"subject": c.Query("subject"),
})
}
// DoSend processes the email composition form, sends the email via SMTP,
// and stores the message record.
func (h *MailHandler) DoSend(c *gin.Context) {
userID := c.GetUint("userID")
currentUserVal, _ := c.Get("currentUser")
currentUser := currentUserVal.(*db.User)
to := c.PostForm("to")
subject := c.PostForm("subject")
body := c.PostForm("body")
cc := c.PostForm("cc")
if to == "" {
c.HTML(http.StatusBadRequest, "compose", gin.H{
"currentUser": currentUser,
"activeFolder": "compose",
"error": "请输入收件人",
"to": to,
"subject": subject,
"cc": cc,
})
return
}
// Build the email content
fromAddr := fmt.Sprintf("%s@%s", currentUser.Username, currentUser.Domain.Name)
now := time.Now()
messageID := fmt.Sprintf("<%s@mail_go>", uuid.New().String())
// Construct the raw email message
var sb strings.Builder
sb.WriteString(fmt.Sprintf("From: %s\r\n", fromAddr))
sb.WriteString(fmt.Sprintf("To: %s\r\n", to))
if cc != "" {
sb.WriteString(fmt.Sprintf("Cc: %s\r\n", cc))
}
sb.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
sb.WriteString(fmt.Sprintf("Message-ID: %s\r\n", messageID))
sb.WriteString(fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z)))
sb.WriteString("MIME-Version: 1.0\r\n")
sb.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
sb.WriteString("\r\n")
sb.WriteString(body)
// Send via local SMTP
err := smtp.SendMail("localhost:25", nil, fromAddr, strings.Split(to, ","), []byte(sb.String()))
if err != nil {
// Log the error but still save to sent folder — SMTP may not be running yet
fmt.Printf("SMTP发送失败(邮件仍保存到发件箱): %v\n", err)
}
// Save to Sent folder
msg := &db.Message{
UserID: userID,
MessageID: messageID,
Folder: "Sent",
FromAddr: fromAddr,
ToAddr: to,
CcAddr: cc,
Subject: subject,
TextBody: body,
Date: now,
IsRead: true,
}
if createErr := h.stores.Mails.Create(msg); createErr != nil {
c.HTML(http.StatusInternalServerError, "compose", gin.H{
"currentUser": currentUser,
"activeFolder": "compose",
"error": fmt.Sprintf("保存邮件失败: %v", createErr),
"to": to,
"subject": subject,
"cc": cc,
})
return
}
// Handle attachments
form, err := c.MultipartForm()
if err == nil {
files := form.File["attachments"]
for _, file := range files {
// Read file content
f, err := file.Open()
if err != nil {
continue
}
buf, err := io.ReadAll(f)
f.Close()
if err != nil {
continue
}
// Save to disk
relPath, err := h.storage.Save(file.Filename, buf)
if err != nil {
continue
}
// Determine content type from extension
contentType := "application/octet-stream"
ext := strings.ToLower(filepath.Ext(file.Filename))
if ct, ok := mimeTypes[ext]; ok {
contentType = ct
}
att := &db.Attachment{
MessageID: msg.ID,
FileName: file.Filename,
FilePath: relPath,
ContentType: contentType,
FileSize: file.Size,
}
_ = h.stores.Attachments.Create(att)
}
}
c.Redirect(http.StatusFound, "/sent")
}
// mimeTypes maps common file extensions to MIME types.
var mimeTypes = map[string]string{
".txt": "text/plain",
".html": "text/html",
".htm": "text/html",
".pdf": "application/pdf",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".zip": "application/zip",
".rar": "application/x-rar-compressed",
".csv": "text/csv",
}
// Sent renders the sent mail folder page.
func (h *MailHandler) Sent(c *gin.Context) {
userID := c.GetUint("userID")
page := getPageParam(c, "page", 1)
messages, total, err := h.stores.Mails.ListByUserAndFolder(userID, "Sent", page, 20)
if err != nil {
c.String(http.StatusInternalServerError, "加载发件箱失败: %v", err)
return
}
currentUser, _ := c.Get("currentUser")
totalPages := int(total) / 20
if int(total)%20 > 0 {
totalPages++
}
if totalPages < 1 {
totalPages = 0
}
c.HTML(200, "sent", gin.H{
"currentUser": currentUser,
"messages": messages,
"total": total,
"page": page,
"pageSize": 20,
"totalPages": totalPages,
"folder": "Sent",
"activeFolder": "sent",
})
}
// Delete removes a message by ID after verifying ownership.
func (h *MailHandler) Delete(c *gin.Context) {
userID := c.GetUint("userID")
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 || msg.UserID != userID {
c.String(http.StatusForbidden, "禁止访问")
return
}
// Delete attachments on disk and in DB
attachments, _ := h.stores.Attachments.ListByMessage(uint(id))
for _, att := range attachments {
_ = h.storage.Delete(att.FilePath)
}
_ = h.stores.Attachments.DeleteByMessage(uint(id))
_ = h.stores.Mails.Delete(uint(id))
// Redirect back based on the folder
referer := c.GetHeader("Referer")
if referer == "" {
referer = "/inbox"
}
c.Redirect(http.StatusFound, referer)
}
// MarkRead marks a message as read.
func (h *MailHandler) MarkRead(c *gin.Context) {
userID := c.GetUint("userID")
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 || msg.UserID != userID {
c.String(http.StatusForbidden, "禁止访问")
return
}
_ = h.stores.Mails.MarkRead(uint(id))
referer := c.GetHeader("Referer")
if referer == "" {
referer = "/inbox"
}
c.Redirect(http.StatusFound, referer)
}
// DownloadAttachment serves an attachment file for download.
func (h *MailHandler) DownloadAttachment(c *gin.Context) {
userID := c.GetUint("userID")
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
}
// Verify the message belongs to the current user
msg, err := h.stores.Mails.GetByID(att.MessageID)
if err != nil || msg.UserID != userID {
c.String(http.StatusForbidden, "禁止访问")
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)
}
// getPageParam extracts and validates a page query parameter.
// Returns defaultVal if the parameter is missing or invalid.
func getPageParam(c *gin.Context, key string, defaultVal int) int {
pageStr := c.Query(key)
if pageStr == "" {
return defaultVal
}
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
return defaultVal
}
return page
}
// Drafts renders the drafts folder page.
func (h *MailHandler) Drafts(c *gin.Context) {
userID := c.GetUint("userID")
page := getPageParam(c, "page", 1)
messages, total, err := h.stores.Mails.ListByUserAndFolder(userID, "Drafts", page, 20)
if err != nil {
c.String(http.StatusInternalServerError, "加载草稿箱失败: %v", err)
return
}
currentUser, _ := c.Get("currentUser")
totalPages := int(total) / 20
if int(total)%20 > 0 {
totalPages++
}
if totalPages < 1 {
totalPages = 0
}
c.HTML(200, "drafts", gin.H{
"currentUser": currentUser,
"messages": messages,
"total": total,
"page": page,
"pageSize": 20,
"totalPages": totalPages,
"folder": "Drafts",
"activeFolder": "drafts",
})
}
// Settings renders the user settings page.
func (h *MailHandler) Settings(c *gin.Context) {
currentUser, _ := c.Get("currentUser")
c.HTML(200, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "",
"success": "",
})
}
// UpdateSettings handles the password change form.
func (h *MailHandler) UpdateSettings(c *gin.Context) {
userID := c.GetUint("userID")
currentUserVal, _ := c.Get("currentUser")
currentUser := currentUserVal.(*db.User)
oldPassword := c.PostForm("old_password")
newPassword := c.PostForm("new_password")
confirmPassword := c.PostForm("confirm_password")
// Verify old password
if err := bcrypt.CompareHashAndPassword([]byte(currentUser.PasswordHash), []byte(oldPassword)); err != nil {
c.HTML(http.StatusBadRequest, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "当前密码不正确",
"success": "",
})
return
}
if newPassword == "" {
c.HTML(http.StatusBadRequest, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "新密码不能为空",
"success": "",
})
return
}
if newPassword != confirmPassword {
c.HTML(http.StatusBadRequest, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "两次输入的密码不一致",
"success": "",
})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
c.HTML(http.StatusInternalServerError, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "密码加密失败",
"success": "",
})
return
}
if err := h.stores.Users.UpdatePassword(userID, string(hashedPassword)); err != nil {
c.HTML(http.StatusInternalServerError, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "密码更新失败",
"success": "",
})
return
}
c.HTML(http.StatusOK, "settings", gin.H{
"currentUser": currentUser,
"activeFolder": "settings",
"error": "",
"success": "密码修改成功",
})
}
// resolveActiveFolder maps a folder name to a sidebar active state key.
func resolveActiveFolder(folder string) string {
switch folder {
case "INBOX":
return "inbox"
case "Sent":
return "sent"
case "Drafts":
return "drafts"
default:
return folder
}
}