Feature/v2.8.9 (#315)

1、翻译优化 #291
2、IMAP协议翻页显示问题修复
3、邮件删除bug修复 #312
This commit is contained in:
Jinnrry
2025-11-23 17:56:07 +08:00
committed by GitHub
parent 14f0fbfa8a
commit bb1da921c8
10 changed files with 241 additions and 45 deletions
+25 -1
View File
@@ -1,3 +1,27 @@
测试imap协议返回
`openssl s_client -crlf -connect imap.xxxx.com:993`
`openssl s_client -crlf -connect imap.xxxx.com:993`
`openssl s_client -crlf -connect 127.0.0.1:993`
删除邮件
```bash
A1 LOGIN testCase testCase
A3 SELECT "Deleted Messages"
A4 UID STORE 1:* +FLAGS.SILENT (\Deleted)
A5 EXPUNGE
```
搜索邮件
```bash
A1 LOGIN testCase testCase
114 SELECT "INBOX"
115 UID SEARCH 1:5 NOT DELETED
```
执行全部测试用例(linux macos不加sudo没有权限监听小于1024的端口,因此会失败)
`sudo env "PATH=$PATH" make test`
执行单个测试用例
`go test -v -run ^Test ./services/del_email`
+4 -4
View File
@@ -16,9 +16,9 @@ import (
func GetUserGroupList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
defaultGroup := []*models.Group{
{models.INBOX, i18n.GetText(ctx.Lang, "inbox"), 0, 0, "/"},
{models.Junk, i18n.GetText(ctx.Lang, "junk"), 0, 0, "/"},
{models.Deleted, i18n.GetText(ctx.Lang, "deleted"), 0, 0, "/"},
{models.INBOX, i18n.GetText(ctx.Lang, "inbox"), 0, 0, "/"}, // 收件箱
{models.Junk, i18n.GetText(ctx.Lang, "junk"), 0, 0, "/"}, //垃圾邮件
{models.Deleted, i18n.GetText(ctx.Lang, "deleted"), 0, 0, "/"}, //已删除
}
infos := group.GetGroupList(ctx)
@@ -42,7 +42,7 @@ func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request
},
{
Label: i18n.GetText(ctx.Lang, "sketch"),
Tag: dto.SearchTag{Type: 1, Status: 0}.ToString(),
Tag: dto.SearchTag{Type: 0, Status: 4}.ToString(),
},
{
Label: i18n.GetText(ctx.Lang, "junk"),
+1 -1
View File
@@ -14,7 +14,7 @@ var (
"login_exp": "登录已失效",
"ip_taps": "这是你服务器IP,确保这个IP正确",
"invalid_email_address": "无效的邮箱地址!",
"deleted": "垃圾箱",
"deleted": "已删除",
"junk": "广告箱",
}
en = map[string]string{
+3 -8
View File
@@ -1,6 +1,7 @@
package http_server
import (
"errors"
"flag"
"fmt"
"github.com/Jinnrry/pmail/config"
@@ -11,7 +12,6 @@ import (
"io/fs"
"net/http"
"os"
"strings"
"time"
)
@@ -34,12 +34,7 @@ func SetupStart() {
flag.Parse()
if HttpPort == 80 {
envs := os.Environ()
for _, env := range envs {
if strings.HasPrefix(env, "setup_port=") {
HttpPort = cast.ToInt(strings.TrimSpace(strings.ReplaceAll(env, "setup_port=", "")))
}
}
HttpPort = cast.ToInt(os.Getenv("setup_port"))
}
if HttpPort <= 0 || HttpPort > 65535 {
@@ -61,7 +56,7 @@ func SetupStart() {
WriteTimeout: time.Second * 60,
}
err = setupServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
if err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
+45 -19
View File
@@ -376,23 +376,32 @@ func TestSearch(t *testing.T) {
func TestMove(t *testing.T) {
clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
_, err := clientLogin.Move(imap.UIDSetNum(21), "Junk").Wait()
res, err := clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
Envelope: true,
Flags: true,
InternalDate: true,
RFC822Size: true,
UID: true,
BodySection: []*imap.FetchItemBodySection{
{
Specifier: imap.PartSpecifierText,
Peek: true,
},
},
}).Collect()
if err != nil {
t.Errorf("%+v", err)
t.Logf("%+v", res)
t.Error("Fetch error")
}
_, err = clientLogin.Move(imap.UIDSetNum(23), "一级菜单").Wait()
if err != nil {
t.Errorf("%+v", err)
}
var ue []models.UserEmail
db.Instance.Table("user_email").Where("id=21 or id=23").Find(&ue)
for _, v := range ue {
if v.ID == 21 && (v.GroupId != 0 || v.Status != 5) {
t.Errorf("TestMove Error")
}
if v.ID == 23 && v.GroupId != 4 {
t.Errorf("TestMove Error")
if len(res) > 0 {
uid := res[0].UID
_, err = clientLogin.Move(imap.UIDSetNum(uid), "Junk").Wait()
if err != nil {
t.Errorf("%+v", err)
}
}
}
@@ -400,14 +409,31 @@ func TestMove(t *testing.T) {
func TestCopy(t *testing.T) {
clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
_, err := clientLogin.Copy(imap.UIDSetNum(25), "Junk").Wait()
res, err := clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
Envelope: true,
Flags: true,
InternalDate: true,
RFC822Size: true,
UID: true,
BodySection: []*imap.FetchItemBodySection{
{
Specifier: imap.PartSpecifierText,
Peek: true,
},
},
}).Collect()
if err != nil {
t.Errorf("%+v", err)
t.Logf("%+v", res)
t.Error("Fetch error")
}
_, err = clientLogin.Copy(imap.UIDSetNum(27), "一级菜单").Wait()
if err != nil {
t.Errorf("%+v", err)
if len(res) > 0 {
_, err = clientLogin.Copy(imap.UIDSetNum(res[0].UID), "Junk").Wait()
if err != nil {
t.Errorf("%+v", err)
}
} else {
t.Error("No Fetch Result")
}
}
@@ -5,6 +5,7 @@ import (
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
"github.com/spf13/cast"
"log/slog"
)
func (s *serverSession) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
@@ -31,6 +32,8 @@ func (s *serverSession) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet)
return nil
}
slog.Debug("DeleteUidList:", slog.Any("uidList", uidList))
err := del_email.DelByUID(s.ctx, uidList)
s.deleteUidList = []int{}
if err != nil {
+101 -2
View File
@@ -37,10 +37,12 @@ func TestMain(m *testing.M) {
httpClient = &http.Client{Jar: cookeieJar, Timeout: 5 * time.Minute}
os.Remove("config/config.json")
os.Remove("config/pmail_temp.db")
os.Setenv("setup_port", cast.ToString(TestPort))
go func() {
main()
}()
time.Sleep(3 * time.Second)
time.Sleep(5 * time.Second)
m.Run()
@@ -79,6 +81,7 @@ func TestMaster(t *testing.T) {
t.Run("testDelEmail", testDelEmail)
t.Run("testSendEmail2User1", testSendEmail2User1)
t.Run("testSendEmail2User12", testSendEmail2User12)
t.Run("testSendEmail2User2", testSendEmail2User2)
t.Run("testSendEmail2User3", testSendEmail2User3)
time.Sleep(8 * time.Second)
@@ -89,6 +92,8 @@ func TestMaster(t *testing.T) {
t.Run("testUser2EmailList", testUser2EmailList)
t.Run("testUser2DelEmail", testUser2DelEmail) // 删除2个人共同拥有的邮件
// 创建group
t.Run("testCreateGroup", testCreateGroup)
@@ -708,6 +713,46 @@ func genTestEmailData(t *testing.T) {
}
func testSendEmail2User12(t *testing.T) {
ret, err := httpClient.Post(TestHost+"/api/email/send", "application/json", strings.NewReader(`
{
"from": {
"name": "i",
"email": "i@test.domain"
},
"to": [
{
"name": "y",
"email": "user1@test.domain"
},
{
"name": "y2",
"email": "user2@test.domain"
}
],
"cc": [
],
"subject": "HelloUser1User2",
"text": "HelloUser1User2",
"html": "<div>HelloUser1User2</div>"
}
`))
if err != nil {
t.Error(err)
}
data, err := readResponse(ret.Body)
if err != nil {
t.Error(err)
}
if data.ErrorNo != 0 {
t.Error("Send Email Api Error!")
}
t.Logf("testSendEmail2User1 Success! Response: %+v", data)
}
func testSendEmail2User1(t *testing.T) {
ret, err := httpClient.Post(TestHost+"/api/email/send", "application/json", strings.NewReader(`
{
@@ -859,7 +904,7 @@ func testUser2EmailList(t *testing.T) {
}
dt := data.Data.(map[string]interface{})
if dt["list"] == nil || len(dt["list"].([]interface{})) != 1 {
if dt["list"] == nil || len(dt["list"].([]interface{})) != 2 {
t.Error("Email List Is Empty!")
}
@@ -909,6 +954,60 @@ func testDelEmail(t *testing.T) {
t.Logf("testDelEmail Success! Response: %+v", data)
}
func testUser2DelEmail(t *testing.T) {
ret, err := httpClient.Post(TestHost+"/api/email/list", "application/json", strings.NewReader(`{}`))
if err != nil {
t.Error(err)
}
data, err := readResponse(ret.Body)
if err != nil {
t.Error(err)
}
if data.ErrorNo != 0 {
t.Error("Get Email List Api Error!")
}
dt := data.Data.(map[string]interface{})
if len(dt["list"].([]interface{})) == 0 {
t.Error("Email List Is Empty!")
}
lst := dt["list"].([]interface{})
for _, item := range lst {
// 删除两个用户的邮件
title := cast.ToString(item.(map[string]interface{})["title"])
id := cast.ToInt(item.(map[string]interface{})["id"])
if title == "HelloUser1User2" {
ret, err = httpClient.Post(TestHost+"/api/email/del", "application/json", strings.NewReader(fmt.Sprintf(`{
"ids":[%d]
}`, id)))
if err != nil {
t.Error(err)
}
data, err = readResponse(ret.Body)
if err != nil {
t.Error(err)
}
if data.ErrorNo != 0 {
t.Error("Email Delete Api Error!")
}
var mails []models.UserEmail
db.Instance.Where("email_id = ?", id).Find(&mails)
for _, mail := range mails {
if mail.Status != 3 && mail.UserID == 3 {
t.Error("Email Delete Api Error!")
}
if mail.UserID != 3 && mail.Status == 3 {
t.Error("Email Delete Api Error!")
}
}
}
}
t.Logf("testDelEmail Success! Response: %+v", data)
}
// portCheck 检查端口是占用
func portCheck(port int) bool {
l, err := net.Listen("tcp", fmt.Sprintf(":%s", strconv.Itoa(port)))
+9 -3
View File
@@ -7,6 +7,7 @@ import (
"github.com/Jinnrry/pmail/utils/context"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
"log/slog"
"xorm.io/xorm"
)
import . "xorm.io/builder"
@@ -64,7 +65,7 @@ func DelByUID(ctx *context.Context, ids []int) error {
defer session.Close()
for _, id := range ids {
var ue models.UserEmail
session.Table("user_email").Where(Eq{"id": ids, "user_id": ctx.UserID}).Get(&ue)
session.Table("user_email").Where(Eq{"id": id, "user_id": ctx.UserID}).Get(&ue)
if ue.ID == 0 {
log.WithContext(ctx).Warn("no user email found")
return nil
@@ -74,6 +75,7 @@ func DelByUID(ctx *context.Context, ids []int) error {
// 先删除关联关系
_, err := session.Table(&models.UserEmail{}).Where("id=? and user_id=?", id, ctx.UserID).Delete(&ue)
if err != nil {
slog.Error("SQLError", slog.Any("err", err))
session.Rollback()
return err
}
@@ -82,12 +84,16 @@ func DelByUID(ctx *context.Context, ids []int) error {
var Num num
_, err = session.Table(&models.UserEmail{}).Select("count(1) as num").Where("email_id=? ", emailId).Get(&Num)
if err != nil {
slog.Error("SQLError", slog.Any("err", err))
session.Rollback()
return err
}
if Num.Num == 0 {
var email models.Email
_, err = session.Table(&email).Where("id=?", id).Delete(&email)
_, err = session.Table(&email).Where("id=?", emailId).Delete(&email)
if err != nil {
slog.Error("SQLError", slog.Any("err", err))
}
}
}
session.Commit()
+19 -3
View File
@@ -218,7 +218,7 @@ func GetGroupStatus(ctx *context.Context, groupName string, params []string) (st
case "MESSAGES":
db.Instance.Table("user_email").Select("count(1)").Where("group_id=?", group.ID).Get(&value)
case "UIDNEXT":
db.Instance.Table("user_email").Select("id").OrderBy("id desc").Get(&value)
db.Instance.Table("user_email").Select("id").Where("group_id=?", group.ID).OrderBy("id desc").Get(&value)
value += 1
case "UIDVALIDITY":
value = group.ID
@@ -242,8 +242,7 @@ func GetGroupStatus(ctx *context.Context, groupName string, params []string) (st
case "MESSAGES":
value = getGroupNum(ctx, groupName, false)
case "UIDNEXT":
db.Instance.Table("user_email").Select("id").OrderBy("id desc").Get(&value)
value += 1
value = getNextUID(ctx, groupName)
case "UIDVALIDITY":
value = models.GroupNameToCode[groupName]
case "UNSEEN":
@@ -262,6 +261,23 @@ func GetGroupStatus(ctx *context.Context, groupName string, params []string) (st
}
func getNextUID(ctx *context.Context, groupName string) int {
var lastId int
switch groupName {
case "INBOX":
db.Instance.Table("user_email").Select("id").Where("user_id=? and group_id=0 and status = 0", ctx.UserID).OrderBy("id desc").Get(&lastId)
case "Sent Messages":
db.Instance.Table("user_email").Select("id").Where("user_id=? and group_id=0 and status = 1", ctx.UserID).OrderBy("id desc").Get(&lastId)
case "Drafts":
db.Instance.Table("user_email").Select("id").Where("user_id=? and group_id=0 and status = 4", ctx.UserID).OrderBy("id desc").Get(&lastId)
case "Deleted Messages":
db.Instance.Table("user_email").Select("id").Where("user_id=? and status = 3", ctx.UserID).OrderBy("id desc").Get(&lastId)
case "Junk":
db.Instance.Table("user_email").Select("id").Where("user_id=? and group_id=0 and status = 5", ctx.UserID).OrderBy("id desc").Get(&lastId)
}
return lastId + 1
}
func getGroupNum(ctx *context.Context, groupName string, mustUnread bool) int {
var count int
switch groupName {
+31 -4
View File
@@ -167,15 +167,42 @@ func GetUEListByUID(ctx *context.Context, groupName string, star, end int, uidLi
func getEmailListByUidList(ctx *context.Context, groupName string, req ImapListReq, uid bool) []*response.EmailResponseData {
var ret []*response.EmailResponseData
var ue []*response.UserEmailUIDData
sql := fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id in (%s))", array.Join(req.UidList, ","))
sql := fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id in (%s) and status = ?)", array.Join(req.UidList, ","))
if req.Star > 0 && req.End != 0 {
sql = fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id >=%d and id <= %d)", req.Star, req.End)
sql = fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id >=%d and id <= %d and status = ?)", req.Star, req.End)
}
if req.Star > 0 && req.End == 0 {
sql = fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id >=%d )", req.Star)
sql = fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id >=%d and status = ?)", req.Star)
}
var err error
switch groupName {
case "INBOX":
err = db.Instance.SQL(sql, ctx.UserID, 0).Find(&ue)
case "Sent Messages":
err = db.Instance.SQL(sql, ctx.UserID, 1).Find(&ue)
case "Drafts":
err = db.Instance.SQL(sql, ctx.UserID, 4).Find(&ue)
case "Deleted Messages":
err = db.Instance.SQL(sql, ctx.UserID, 3).Find(&ue)
case "Junk":
err = db.Instance.SQL(sql, ctx.UserID, 5).Find(&ue)
default:
groupNames := strings.Split(groupName, "/")
groupName = groupNames[len(groupNames)-1]
var group models.Group
db.Instance.Table("group").Where("user_id=? and name=?", ctx.UserID, groupName).Get(&group)
if group.ID == 0 {
return ret
}
err = db.Instance.
SQL(fmt.Sprintf(
"SELECT * from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and group_id = ?)) a WHERE serial_number in (%s)",
array.Join(req.UidList, ","))).
Find(&ue, ctx.UserID, group.ID)
}
err := db.Instance.SQL(sql, ctx.UserID).Find(&ue)
if err != nil {
log.WithContext(ctx).Errorf("SQL ERROR: %s ,Error:%s", sql, err)
}