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