Feature/v2.9.2 (#344)

* feature/v2.9.0

ui优化

* add test case

add test case

* feature/v2.9.2

bug fix

---------

Co-authored-by: Your Name <you@example.com>
This commit is contained in:
Jinnrry 2026-05-31 18:42:58 +08:00 committed by GitHub
parent 68dc2f0f47
commit 6455635f74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 257 additions and 66 deletions

View File

@ -31,16 +31,16 @@
</div>
<div class="list-content">
<el-table
ref="taskTableDataRef"
:data="data"
:show-header="false"
class="modern-mail-table"
@row-click="rowClick"
:row-style="rowStyle"
<el-table height="100%"
ref="taskTableDataRef"
:data="data"
:show-header="false"
class="modern-mail-table"
@row-click="rowClick"
:row-style="rowStyle"
>
<el-table-column type="selection" width="40"/>
<el-table-column width="30" class-name="status-col">
<template #default="scope">
<div class="status-indicator">
@ -75,11 +75,11 @@
</div>
<div class="pagination-wrapper" v-if="totalPage > 0">
<el-pagination
background
layout="prev, pager, next"
:page-count="totalPage"
@current-change="pageChange"
<el-pagination
background
layout="prev, pager, next"
:page-count="totalPage"
@current-change="pageChange"
/>
</div>
</div>
@ -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;

View File

@ -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)

View File

@ -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(),
},
}

View File

@ -42,7 +42,7 @@ func Init(version string) error {
}
Instance.SetConnMaxLifetime(30 * time.Minute)
Instance.ShowSQL(false)
Instance.ShowSQL(config.Instance.LogLevel == "debug")
// 同步表结构
syncTables()

View File

@ -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 {

View File

@ -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
}

View File

@ -139,7 +139,6 @@ func (s *Session) Data(r io.Reader) error {
} else {
// 收件
var dkimStatus, SPFStatus bool
// DKIM校验

View File

@ -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=;

View File

@ -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{

View File

@ -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 != "" {

View File

@ -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)
}