diff --git a/fe/src/views/ListView.vue b/fe/src/views/ListView.vue
index a13cb4b..94b60a2 100644
--- a/fe/src/views/ListView.vue
+++ b/fe/src/views/ListView.vue
@@ -31,16 +31,16 @@
-
-
+
@@ -75,11 +75,11 @@
@@ -171,7 +171,7 @@ const move = function (group_id, group_name) {
return;
}
let ids = rows.map(e => e.id);
-
+
ElMessageBox.confirm(lang.move_email_confirm, 'Warning', {
confirmButtonText: 'OK', cancelButtonText: 'Cancel', type: 'warning'
}).then(() => {
@@ -224,7 +224,9 @@ const pageChange = function (p) {
.list-view-container {
display: flex;
flex-direction: column;
+ /* 【修复核心】让其优先贴合父元素高度,保底视口高度 */
height: 100%;
+ min-height: 100vh;
background: var(--pm-surface-glass);
border: 1px solid var(--pm-glass-border);
border-radius: var(--pm-radius-xl);
@@ -236,6 +238,7 @@ const pageChange = function (p) {
}
.list-header {
+ flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
@@ -290,11 +293,12 @@ const pageChange = function (p) {
}
.list-content {
- flex-grow: 1;
+ min-height: 0;
+ flex: 1;
+ overflow: hidden;
background: var(--pm-surface-glass);
border-radius: var(--pm-radius-lg);
border: 1px solid var(--pm-border-color);
- overflow: hidden;
box-shadow: inset 0 1px 0 var(--pm-glass-border);
}
@@ -330,9 +334,8 @@ const pageChange = function (p) {
.mail-main-info {
display: flex;
align-items: center;
- gap: 16px;
- flex-grow: 1;
- overflow: hidden;
+ width: 100%;
+ gap: 12px;
}
.mail-sender {
@@ -347,23 +350,22 @@ const pageChange = function (p) {
}
.mail-subject {
- font-weight: 500;
- color: var(--pm-text-primary);
+ flex: 0 1 auto;
+ max-width: 40%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 420px;
- font-size: 14px;
+ font-weight: 500;
+ color: #222;
}
.mail-snippet {
- color: var(--pm-text-muted);
+ flex: 1;
+ min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- font-size: 14px;
- flex-grow: 1;
- opacity: 0.95;
+ color: #999;
}
.mail-meta {
@@ -390,10 +392,13 @@ const pageChange = function (p) {
}
.pagination-wrapper {
+ flex-shrink: 0;
+ padding: 10px 0;
+ padding-bottom: calc(10px + env(safe-area-inset-bottom)); /* 【修复核心】适配 iPhone 底部小黑条 */
+ background-color: transparent; /* 【修复核心】去除白色背景避免暗黑模式突兀 */
margin-top: 14px;
display: flex;
justify-content: center;
- padding-bottom: 4px;
}
@media (prefers-color-scheme: dark) {
@@ -403,6 +408,13 @@ const pageChange = function (p) {
}
@media (max-width: 768px) {
+ .list-view-container {
+ /* 【修复核心】移动端强制计算高度,减去顶部 Mobile Header 高度 (通常约为 56px),使用 dvh 避免滚动遮挡 */
+ height: calc(100dvh - 56px);
+ min-height: auto;
+ padding: 12px; /* 移动端适当减小 padding,留出更多可视空间 */
+ }
+
.list-header {
flex-direction: column;
align-items: flex-start;
@@ -424,7 +436,7 @@ const pageChange = function (p) {
max-width: 100%;
}
.mail-snippet {
- display: none; /* Hide snippet on very small screens to save space */
+ display: none;
}
.mail-row-content {
align-items: flex-start;
diff --git a/server/controllers/email/list.go b/server/controllers/email/list.go
index beac5f8..4f49af6 100644
--- a/server/controllers/email/list.go
+++ b/server/controllers/email/list.go
@@ -77,6 +77,11 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
var sender User
_ = json.Unmarshal([]byte(email.Sender), &sender)
+ if sender.EmailAddress == "" {
+ sender.EmailAddress = email.FromAddress
+ sender.Name = email.FromName
+ }
+
var tos []User
_ = json.Unmarshal([]byte(email.To), &tos)
diff --git a/server/controllers/group.go b/server/controllers/group.go
index 307960b..9b6f09e 100644
--- a/server/controllers/group.go
+++ b/server/controllers/group.go
@@ -30,29 +30,24 @@ func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request
retData := []*group.GroupItem{
{
- Label: i18n.GetText(ctx.Lang, "all_email"),
- Children: []*group.GroupItem{
- {
- Label: i18n.GetText(ctx.Lang, "inbox"),
- Tag: dto.SearchTag{Type: 0, Status: -1, GroupId: 0}.ToString(),
- },
- {
- Label: i18n.GetText(ctx.Lang, "outbox"),
- Tag: dto.SearchTag{Type: 1, Status: -1}.ToString(),
- },
- {
- Label: i18n.GetText(ctx.Lang, "sketch"),
- Tag: dto.SearchTag{Type: 0, Status: 4}.ToString(),
- },
- {
- Label: i18n.GetText(ctx.Lang, "junk"),
- Tag: dto.SearchTag{Type: -1, Status: 5}.ToString(),
- },
- {
- Label: i18n.GetText(ctx.Lang, "deleted"),
- Tag: dto.SearchTag{Type: -1, Status: 3}.ToString(),
- },
- },
+ Label: i18n.GetText(ctx.Lang, "inbox"),
+ Tag: dto.SearchTag{Type: 0, Status: -1, GroupId: 0}.ToString(),
+ },
+ {
+ Label: i18n.GetText(ctx.Lang, "outbox"),
+ Tag: dto.SearchTag{Type: 1, Status: -1}.ToString(),
+ },
+ {
+ Label: i18n.GetText(ctx.Lang, "sketch"),
+ Tag: dto.SearchTag{Type: 0, Status: 4}.ToString(),
+ },
+ {
+ Label: i18n.GetText(ctx.Lang, "junk"),
+ Tag: dto.SearchTag{Type: -1, Status: 5}.ToString(),
+ },
+ {
+ Label: i18n.GetText(ctx.Lang, "deleted"),
+ Tag: dto.SearchTag{Type: -1, Status: 3}.ToString(),
},
}
diff --git a/server/db/init.go b/server/db/init.go
index fbe014c..832659e 100644
--- a/server/db/init.go
+++ b/server/db/init.go
@@ -42,7 +42,7 @@ func Init(version string) error {
}
Instance.SetConnMaxLifetime(30 * time.Minute)
- Instance.ShowSQL(false)
+ Instance.ShowSQL(config.Instance.LogLevel == "debug")
// 同步表结构
syncTables()
diff --git a/server/listen/imap_server/session_move.go b/server/listen/imap_server/session_move.go
index 58eb458..ade8f27 100644
--- a/server/listen/imap_server/session_move.go
+++ b/server/listen/imap_server/session_move.go
@@ -38,6 +38,10 @@ func (s *serverSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, dest
mailIds = append(mailIds, email.Id)
}
+ if mailIds == nil || len(mailIds) == 0 {
+ return nil
+ }
+
if group.IsDefaultBox(dest) {
return move2defaultbox(s.ctx, mailIds, dest)
} else {
diff --git a/server/listen/imap_server/session_store.go b/server/listen/imap_server/session_store.go
index 5457113..a3fc379 100644
--- a/server/listen/imap_server/session_store.go
+++ b/server/listen/imap_server/session_store.go
@@ -2,6 +2,7 @@ package imap_server
import (
"github.com/Jinnrry/pmail/dto/response"
+ "github.com/Jinnrry/pmail/services/del_email"
"github.com/Jinnrry/pmail/services/detail"
"github.com/Jinnrry/pmail/services/list"
"github.com/Jinnrry/pmail/utils/array"
@@ -52,5 +53,15 @@ func (s *serverSession) Store(w *imapserver.FetchWriter, numSet imap.NumSet, fla
}
}
+ err := del_email.DelByUID(s.ctx, s.deleteUidList)
+ s.deleteUidList = []int{}
+
+ if err != nil {
+ return &imap.Error{
+ Type: imap.StatusResponseTypeNo,
+ Text: err.Error(),
+ }
+ }
+
return nil
}
diff --git a/server/listen/smtp_server/read_content.go b/server/listen/smtp_server/read_content.go
index 4a24a51..e9c001c 100644
--- a/server/listen/smtp_server/read_content.go
+++ b/server/listen/smtp_server/read_content.go
@@ -139,7 +139,6 @@ func (s *Session) Data(r io.Reader) error {
} else {
// 收件
-
var dkimStatus, SPFStatus bool
// DKIM校验
diff --git a/server/listen/smtp_server/read_content_test.go b/server/listen/smtp_server/read_content_test.go
index 80d2317..1b154d8 100644
--- a/server/listen/smtp_server/read_content_test.go
+++ b/server/listen/smtp_server/read_content_test.go
@@ -48,6 +48,95 @@ func testInit() {
hooks.Init("dev")
}
+func TestGmail(t *testing.T) {
+ testInit()
+ emailData := `Received: by mail-dl1-f46.google.com with SMTP id a92af1059eb24-132c338a537so5798485c88.0
+ for ; Sun, 24 May 2026 08:18:19 -0700 (PDT)
+ARC-Seal: i=1; a=rsa-sha256; t=1779635898; cv=none;
+ d=google.com; s=arc-20240605;
+ b=T2MoTE0s0bFADLxUOXXGL5f6JyO2V39Pcae2DJ21wpFO3D5b8YXzGP5Qq8BO5y3ya+
+ a2Huxddo3xEJikr9FnvBztxWuwyekSpqM+/NH8Do/ZTEconsziakmma08oLO+Uw5+fxO
+ OmV3oo9N2MtWm7nM1EGkUZ3H1EpEAE7nfQ//gXrEGog+u3dKLns5aS9LHl7fjmHUK4ll
+ +mWj3AcbiBLjOzfV2iSCq8kLD8QgN8kDugXzsMLoXxchzblMMw/G6Z1gMTDtRyFPdEOp
+ eKn70lz4dRJyGIdsPbS2N4hSBLNHMrQ3YIUDJ3UO2/HfTG8uenKAQznt5jZJixKa9hiT
+ +W+g==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
+ h=to:subject:message-id:date:from:mime-version:dkim-signature;
+ bh=krMNvdL0wIWIbww3tkM/xlqwm94sXWplcXqGF4i93iM=;
+ fh=6iav8edVevT+IuZxBKkoHjEHmIBsC8UMPcjZALicuAw=;
+ b=WqsmQI22ntkTMVXOTnajcX141N5/B/Xh6R0N5e+LmT8WzQAPNOPK7r/o7ALBmPEA/t
+ 3y5z8EsUhxNUnSaQmDoRGz4AhC/PvDlxVhE4SS0rEVGxJFmCwpPFb+QhgDEyRh0WUqcO
+ /4hQ7rFqWN7oJb5YBuJOX241VHW8jFrQAuzgdLPQXSYPz7Rc4gL0pDIj/1GfWVKwEqbT
+ U/oB7u99oaagwwN1u5U2kphaTxCmaa1syxqpNFEcirDGMwf5WaNpOuFj0zgIip7SMEJY
+ AtD0WxQaO2fN7gbiDO62ZoqkgFjKJ3G3psreDP0oS1zXRn8xHeBFnoqUXOVhZCIDlVmb
+ FT2A==;
+ darn=linuxuser.site
+ARC-Authentication-Results: i=1; mx.google.com; arc=none
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=gmail.com; s=20251104; t=1779635898; x=1780240698; darn=linuxuser.site;
+ h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
+ :date:message-id:reply-to;
+ bh=krMNvdL0wIWIbww3tkM/xlqwm94sXWplcXqGF4i93iM=;
+ b=c/8+TeXpadbyFUf68OwKCCq5Oot/asRNC38R0/hUp1gGm6rJvV9ly7ffClrFogdbH+
+ SJRUFMNBDuu5/Tzyuk/4SlWK5sEWC29y0i00pVmOFoet55hm58mLgdmrtpIdCQ6L773R
+ 7akIMo3Q6D074PUiK/pxOEGUNp2A7pmZb2q9ZQUeZ/dfblgJh6Q/u+8N1o+nDwabnUPF
+ H/nRaXoA3YdTt0s5tXr0+wDLW4MpXOudo2Of2ELHOzmXA93WJDE1dWNX/xuaF5W9s4nk
+ dVt4A97Zui4t3cPdZEjvrG6yY+t5Fu5gKmVVpVTtvRJITWIqhzxiaxWtQtlbKLK+LD8U
+ 3nfQ==
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=1e100.net; s=20251104; t=1779635898; x=1780240698;
+ h=to:subject:message-id:date:from:mime-version:x-gm-gg
+ :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to;
+ bh=krMNvdL0wIWIbww3tkM/xlqwm94sXWplcXqGF4i93iM=;
+ b=LLWfy51jPWQLEt/9Vs8TzwMojaDTfB7cUcYc5IdQAn7SScU+N4kqeTLb8aK9fmTeX/
+ G8ZytpEbKbXAyHtNxh0ezs5ro03UljCTEzKZGEcB2Qy0Idjd2vY9saRXbFU/7h7/BdNm
+ Tztf1K5Wvf+ZmQyYGVLb6yJbGv/RTql6ZsLcym+aWv6JEkVtRVMwJ/IHm951eQrur8KE
+ tsiOafkuHRZzkSDSq8hqw61/v/APnjuTVB66UhC8hbAErav9tlIAScqbzSwSimxB/Aj0
+ 2OTvo6hHRCsc+I2/6gtkTDZKam/DWVs/hA9hURGRvQORos7Yk3r5snAn5mHB1ybJCrpw
+ 7New==
+X-Gm-Message-State: AOJu0Yy7VeDNPEFVk1g6RqPM+hyzjrIVL2deO813Iq4W+3/H6+06AsZG
+ vQbBZjD+YbasztrgwM8FGpqRDiF7NlauyDQyhPzL9NZ7TKa0y0Q5gWah+FKMBcXFOI7r11JyTb/
+ 5Dh6yLwCQ2BWrluabz0fPRL/XUXQNmwFGS9du1f4=
+X-Gm-Gg: Acq92OFQjCPQ1C3jnZQayhjIMDz/3uQ0AVjPNBUEdpuEvw5pkJXnD7s6j8PEKgf9jUA
+ gbc+koh+f+m2686BdRuMX58AIQ689Q4WpcdEq4lRpbKZpoasjniA5u1l/lBI1/+i0olLrkHO0LF
+ EF5QEemC93jbaR8hB7EMPoZ4zmIIe0tOqaChCMthhtKp/df2kiLc+BIFVJR04EKqEi48tsdOrrM
+ 6qNGo3oufUSAFfhJKUNUF88UhOzWAyfc6t9lEdby+Q+LwOopdenMnV837OlsTIpQMKHJayw6aoW
+ 049SxRoJ/Q==
+X-Received: by 2002:a05:7022:6b95:b0:123:3c24:b15 with SMTP id
+ a92af1059eb24-136340ce619mr5527125c88.19.1779635898132; Sun, 24 May 2026
+ 08:18:18 -0700 (PDT)
+MIME-Version: 1.0
+From: Ming Deer
+Date: Sun, 24 May 2026 23:18:07 +0800
+X-Gm-Features: AVHnY4KsQv0XWokXwRtSxwial70kRdR-hEcbdeCsV7DylqP4mkAA0SzorBeWNMA
+Message-ID:
+Subject: =?UTF-8?B?5Li76aKY5rWL6K+VMg==?=
+To: xfox@linuxuser.site
+Content-Type: multipart/alternative; boundary="00000000000089ad93065291c5f4"
+
+--00000000000089ad93065291c5f4
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: base64
+
+5q2j5paH5rWL6K+VMg0K
+--00000000000089ad93065291c5f4
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: base64
+
+PGRpdiBkaXI9Imx0ciI+PGRpdj7mraPmlofmtYvor5UyPC9kaXY+PC9kaXY+DQo=
+--00000000000089ad93065291c5f4--
+`
+
+ s := Session{
+ RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+ Ctx: &context.Context{},
+ To: []string{"xfox@linuxuser.site"},
+ }
+
+ s.Data(bytes.NewReader([]byte(emailData)))
+
+}
+
func TestPmailEmail(t *testing.T) {
testInit()
emailData := `DKIM-Signature: a=rsa-sha256; bh=x7Rh+N2y2K9exccEAyKCTAGDgYKfnLZpMWc25ug5Ny4=;
diff --git a/server/models/group.go b/server/models/group.go
index 65549a8..c822398 100644
--- a/server/models/group.go
+++ b/server/models/group.go
@@ -9,11 +9,11 @@ type Group struct {
}
const (
- INBOX = 2000000000
- Sent = 2000000001
- Drafts = 2000000002
- Deleted = 2000000003
- Junk = 2000000004
+ INBOX = 2000000000 //收件箱
+ Sent = 2000000001 //发件箱
+ Drafts = 2000000002 //草稿箱
+ Deleted = 2000000003 //垃圾箱
+ Junk = 2000000004 //广告箱
)
var GroupNameToCode = map[string]int{
diff --git a/server/services/list/list.go b/server/services/list/list.go
index ff69f2e..4edb41a 100644
--- a/server/services/list/list.go
+++ b/server/services/list/list.go
@@ -2,6 +2,7 @@ package list
import (
"fmt"
+ "github.com/Jinnrry/pmail/consts"
"github.com/Jinnrry/pmail/db"
"github.com/Jinnrry/pmail/dto"
"github.com/Jinnrry/pmail/dto/response"
@@ -48,8 +49,18 @@ func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword str
}
if tagInfo.Status != -1 {
- sql += " and ue.status =? "
- sqlParams = append(sqlParams, tagInfo.Status)
+ switch tagInfo.Status {
+ case consts.EmailStatusDel:
+ sql += " and (ue.status =? or ue.group_id=?) "
+ sqlParams = append(sqlParams, tagInfo.Status, models.Deleted)
+ case consts.EmailStatusDrafts:
+ sql += " and (ue.status =? or ue.group_id=?) "
+ sqlParams = append(sqlParams, tagInfo.Status, models.Drafts)
+ case consts.EmailStatusJunk:
+ sql += " and (ue.status =? or ue.group_id=?) "
+ sqlParams = append(sqlParams, tagInfo.Status, models.Junk)
+ }
+
} else if tagInfo.Status == -1 {
if tagInfo.Type != 1 {
sql += " and ue.status = 0"
@@ -59,16 +70,25 @@ func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword str
}
}
- if tagInfo.Type != -1 {
+ if tagInfo.Type == consts.EmailTypeReceive {
sql += " and type =? "
sqlParams = append(sqlParams, tagInfo.Type)
+ } else if tagInfo.Type == consts.EmailTypeSend {
+ sql += " and (type =? or ue.group_id=?)"
+ sqlParams = append(sqlParams, tagInfo.Type, models.Sent)
}
if tagInfo.GroupId != -1 {
- sql += " and ue.group_id=? "
- sqlParams = append(sqlParams, tagInfo.GroupId)
+ if tagInfo.GroupId > 0 {
+ sql += " and ue.group_id=? "
+ sqlParams = append(sqlParams, tagInfo.GroupId)
+ } else if tagInfo.GroupId == 0 && tagInfo.Status == -1 {
+ sql += " and (ue.group_id=? or ue.group_id=0)"
+ sqlParams = append(sqlParams, models.INBOX)
+ }
} else {
- sql += " and ue.group_id=0 "
+ sql += " and (ue.group_id=0 or ue.group_id =?) "
+ sqlParams = append(sqlParams, models.INBOX)
}
if keyword != "" {
diff --git a/server/services/rule/rule.go b/server/services/rule/rule.go
index d7f523f..5ac2672 100644
--- a/server/services/rule/rule.go
+++ b/server/services/rule/rule.go
@@ -86,7 +86,63 @@ func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email, user *
log.WithContext(ctx).Errorf("Forward Error:%v", err)
}
case dto.MOVE:
- _, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).Cols("group_id").Update(map[string]interface{}{"group_id": cast.ToInt(rule.Params)})
+ doMove(ctx, rule, email, user)
+ }
+
+}
+
+func doMove(ctx *context.Context, rule *dto.Rule, email *parsemail.Email, user *models.User) {
+
+ groupId := cast.ToInt(rule.Params)
+ switch groupId {
+ case models.INBOX:
+ _, err := db.Instance.Table(&models.Email{}).Where("id=?", email.MessageId).
+ Cols("type").Update(map[string]interface{}{"type": consts.EmailTypeReceive})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+
+ _, err = db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
+ Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": 0})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+ case models.Sent:
+ _, err := db.Instance.Table(&models.Email{}).Where("id=?", email.MessageId).
+ Cols("type").Update(map[string]interface{}{"type": consts.EmailTypeSend})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+
+ _, err = db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
+ Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": 0})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+
+ case models.Drafts:
+ _, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
+ Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": consts.EmailStatusDrafts})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+
+ case models.Deleted:
+ _, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
+ Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": consts.EmailStatusDel})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+ case models.Junk:
+ _, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
+ Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": consts.EmailStatusJunk})
+ if err != nil {
+ log.WithContext(ctx).Errorf("sqlERror :%v", err)
+ }
+
+ default:
+
+ _, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).Cols("group_id").Update(map[string]interface{}{"group_id": groupId})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}