diff --git a/docs/debug.md b/docs/debug.md index b03dadd..13481ee 100644 --- a/docs/debug.md +++ b/docs/debug.md @@ -1,3 +1,27 @@ 测试imap协议返回 -`openssl s_client -crlf -connect imap.xxxx.com:993` \ No newline at end of file +`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` \ No newline at end of file diff --git a/server/controllers/group.go b/server/controllers/group.go index dfb2d80..307960b 100644 --- a/server/controllers/group.go +++ b/server/controllers/group.go @@ -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"), diff --git a/server/i18n/i18n.go b/server/i18n/i18n.go index 76c5771..f8330e4 100644 --- a/server/i18n/i18n.go +++ b/server/i18n/i18n.go @@ -14,7 +14,7 @@ var ( "login_exp": "登录已失效", "ip_taps": "这是你服务器IP,确保这个IP正确", "invalid_email_address": "无效的邮箱地址!", - "deleted": "垃圾箱", + "deleted": "已删除", "junk": "广告箱", } en = map[string]string{ diff --git a/server/listen/http_server/setup_server.go b/server/listen/http_server/setup_server.go index bdc1870..f93d6b3 100644 --- a/server/listen/http_server/setup_server.go +++ b/server/listen/http_server/setup_server.go @@ -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) } } diff --git a/server/listen/imap_server/imap_server_test.go b/server/listen/imap_server/imap_server_test.go index af619bb..3bdaab6 100644 --- a/server/listen/imap_server/imap_server_test.go +++ b/server/listen/imap_server/imap_server_test.go @@ -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") } } diff --git a/server/listen/imap_server/session_expunge.go b/server/listen/imap_server/session_expunge.go index eb9feeb..67ce7e6 100644 --- a/server/listen/imap_server/session_expunge.go +++ b/server/listen/imap_server/session_expunge.go @@ -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 { diff --git a/server/main_test.go b/server/main_test.go index a40da7c..0b6c4db 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -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": "
HelloUser1User2
" +} + +`)) + 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))) diff --git a/server/services/del_email/del_email.go b/server/services/del_email/del_email.go index 151150d..e6274ec 100644 --- a/server/services/del_email/del_email.go +++ b/server/services/del_email/del_email.go @@ -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() diff --git a/server/services/group/group.go b/server/services/group/group.go index 21e81c3..7fba683 100644 --- a/server/services/group/group.go +++ b/server/services/group/group.go @@ -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 { diff --git a/server/services/list/list.go b/server/services/list/list.go index 60c540a..ff69f2e 100644 --- a/server/services/list/list.go +++ b/server/services/list/list.go @@ -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) }