diff --git a/server/controllers/group.go b/server/controllers/group.go index 5b4723c..9832e1c 100644 --- a/server/controllers/group.go +++ b/server/controllers/group.go @@ -36,6 +36,10 @@ 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(), }, + { + 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 31aec12..f4f5eab 100644 --- a/server/db/init.go +++ b/server/db/init.go @@ -56,7 +56,11 @@ func Init(version string) error { if version != "" && v.Info != version && version != "test" { v.Info = version - Instance.Update(&v) + if v.Id == 0 { + Instance.Insert(&v) + } else { + Instance.Update(&v) + } } if config.Instance.LogLevel == "debug" { diff --git a/server/hooks/spam_block/spam_block.go b/server/hooks/spam_block/spam_block.go index fca99b0..f9a39ca 100644 --- a/server/hooks/spam_block/spam_block.go +++ b/server/hooks/spam_block/spam_block.go @@ -157,7 +157,7 @@ func (s *SpamBlock) ReceiveParseAfter(ctx *context.Context, email *parsemail.Ema } if maxClass != 0 && maxScore > s.cfg.Threshold/100 { - email.Status = 3 + email.Status = 5 } } diff --git a/server/i18n/i18n.go b/server/i18n/i18n.go index 1fa4a1d..76c5771 100644 --- a/server/i18n/i18n.go +++ b/server/i18n/i18n.go @@ -15,6 +15,7 @@ var ( "ip_taps": "这是你服务器IP,确保这个IP正确", "invalid_email_address": "无效的邮箱地址!", "deleted": "垃圾箱", + "junk": "广告箱", } en = map[string]string{ "all_email": "All Email", @@ -30,6 +31,7 @@ var ( "ip_taps": "This is your server's IP, make sure it is correct.", "invalid_email_address": "Invalid e-mail address!", "deleted": "Deleted", + "junk": "Junk", } ) diff --git a/server/listen/imap_server/imap_server.go b/server/listen/imap_server/imap_server.go index a65b014..cd2715e 100644 --- a/server/listen/imap_server/imap_server.go +++ b/server/listen/imap_server/imap_server.go @@ -37,7 +37,6 @@ func StarTLS() { }, Caps: imap.CapSet{ imap.CapIMAP4rev1: {}, - imap.CapIMAP4rev2: {}, }, TLSConfig: tlsConfig, InsecureAuth: false, diff --git a/server/listen/imap_server/imap_server_test.go b/server/listen/imap_server/imap_server_test.go index 5c14fa1..af619bb 100644 --- a/server/listen/imap_server/imap_server_test.go +++ b/server/listen/imap_server/imap_server_test.go @@ -400,24 +400,16 @@ func TestMove(t *testing.T) { func TestCopy(t *testing.T) { clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait() - res, err := clientLogin.Copy(imap.UIDSetNum(25), "Junk").Wait() + _, err := clientLogin.Copy(imap.UIDSetNum(25), "Junk").Wait() if err != nil { t.Errorf("%+v", err) } - t.Logf("%+v", res) - - if !res.DestUIDs.Contains(33) { - t.Errorf("TestCopy Error") - } - res, err = clientLogin.Copy(imap.UIDSetNum(27), "一级菜单").Wait() + _, err = clientLogin.Copy(imap.UIDSetNum(27), "一级菜单").Wait() if err != nil { t.Errorf("%+v", err) } - t.Logf("%+v", res) - if !res.DestUIDs.Contains(34) { - t.Errorf("TestCopy Error") - } + } func TestNoop(t *testing.T) { diff --git a/server/listen/imap_server/server.go b/server/listen/imap_server/server.go index 9e95c9b..de4524f 100644 --- a/server/listen/imap_server/server.go +++ b/server/listen/imap_server/server.go @@ -38,6 +38,7 @@ type serverSession struct { status Status currentMailbox string connectTime time.Time + deleteUidList []int } // NewSession creates a new IMAP session. diff --git a/server/listen/imap_server/session_expunge.go b/server/listen/imap_server/session_expunge.go index 982555f..eb9feeb 100644 --- a/server/listen/imap_server/session_expunge.go +++ b/server/listen/imap_server/session_expunge.go @@ -8,20 +8,31 @@ import ( ) func (s *serverSession) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error { - if uids == nil { + if uids == nil && len(s.deleteUidList) == 0 { return nil } - uidList := []int{} - for _, uidRange := range *uids { - if uidRange.Start > 0 && uidRange.Stop > 0 { - for i := uidRange.Start; i <= uidRange.Stop; i++ { - uidList = append(uidList, cast.ToInt(uint32(i))) + + if uids != nil { + for _, uidRange := range *uids { + if uidRange.Start > 0 && uidRange.Stop > 0 { + for i := uidRange.Start; i <= uidRange.Stop; i++ { + uidList = append(uidList, cast.ToInt(uint32(i))) + } } } } + if len(s.deleteUidList) > 0 { + uidList = append(uidList, s.deleteUidList...) + } + + if len(uidList) == 0 { + return nil + } + err := del_email.DelByUID(s.ctx, uidList) + s.deleteUidList = []int{} if err != nil { return &imap.Error{ Type: imap.StatusResponseTypeNo, diff --git a/server/listen/imap_server/session_fetch.go b/server/listen/imap_server/session_fetch.go index 9030479..a3ebc32 100644 --- a/server/listen/imap_server/session_fetch.go +++ b/server/listen/imap_server/session_fetch.go @@ -76,7 +76,15 @@ func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*respons if section.Specifier == imap.PartSpecifierHeader { var b bytes.Buffer parseEmail := parsemail.NewEmailFromModel(email.Email) - for _, field := range section.HeaderFields { + fields := section.HeaderFields + + if fields == nil || len(fields) == 0 { + fields = []string{ + "date", "subject", "from", "to", "cc", "message-id", "content-type", + } + } + + for _, field := range fields { switch field { case "date": fmt.Fprintf(&b, "Date: %s\r\n", email.CreateTime.Format(time.RFC1123Z)) diff --git a/server/listen/imap_server/session_search.go b/server/listen/imap_server/session_search.go index 0ccfe03..55bf381 100644 --- a/server/listen/imap_server/session_search.go +++ b/server/listen/imap_server/session_search.go @@ -5,7 +5,6 @@ import ( "github.com/Jinnrry/pmail/services/list" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapserver" - log "github.com/sirupsen/logrus" "github.com/spf13/cast" ) @@ -14,7 +13,7 @@ func (s *serverSession) Search(kind imapserver.NumKind, criteria *imap.SearchCri for _, uidSet := range criteria.UID { for _, uid := range uidSet { - res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uid.Start), cast.ToInt(uid.Stop), nil) + res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uint32(uid.Start)), cast.ToInt(uint32(uid.Stop)), nil) retList = append(retList, res...) } } @@ -23,24 +22,24 @@ func (s *serverSession) Search(kind imapserver.NumKind, criteria *imap.SearchCri if kind == imapserver.NumKindSeq { idList := imap.SeqSet{} for _, data := range retList { - log.WithContext(s.ctx).Debugf("Search Seq result: UID: %d EmailID:%d", data.ID, data.EmailID) idList = append(idList, imap.SeqRange{ Start: cast.ToUint32(data.SerialNumber), Stop: cast.ToUint32(data.SerialNumber), }) } ret.All = idList + ret.Count = uint32(len(retList)) } else { idList := imap.UIDSet{} for _, data := range retList { - log.WithContext(s.ctx).Debugf("Search UID result: UID: %d EmailID:%d", data.ID, data.EmailID) - idList = append(idList, imap.UIDRange{ Start: imap.UID(data.ID), Stop: imap.UID(data.ID), }) } + ret.UID = true ret.All = idList + ret.Count = uint32(len(retList)) } return ret, nil } diff --git a/server/listen/imap_server/session_store.go b/server/listen/imap_server/session_store.go index 06fd54d..5457113 100644 --- a/server/listen/imap_server/session_store.go +++ b/server/listen/imap_server/session_store.go @@ -1,6 +1,7 @@ package imap_server import ( + "github.com/Jinnrry/pmail/dto/response" "github.com/Jinnrry/pmail/services/detail" "github.com/Jinnrry/pmail/services/list" "github.com/Jinnrry/pmail/utils/array" @@ -10,38 +11,46 @@ import ( ) func (s *serverSession) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error { + if flags.Op == imap.StoreFlagsSet { return nil } - if !array.InArray(imap.FlagSeen, flags.Flags) { - return nil - } + var emailList []*response.EmailResponseData switch numSet.(type) { case imap.SeqSet: seqSet := numSet.(imap.SeqSet) for _, seq := range seqSet { - emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + res := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ Star: cast.ToInt(seq.Start), End: cast.ToInt(seq.Stop), }, false) - for _, data := range emailList { - detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd) - } + emailList = append(emailList, res...) } case imap.UIDSet: uidSet := numSet.(imap.UIDSet) for _, uid := range uidSet { - emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + res := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ Star: cast.ToInt(uint32(uid.Start)), End: cast.ToInt(uint32(uid.Stop)), }, true) - for _, data := range emailList { - detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd) - } + emailList = append(emailList, res...) + } + } + + if array.InArray(imap.FlagSeen, flags.Flags) && flags.Op == imap.StoreFlagsAdd { + for _, data := range emailList { + detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd) } } + + if array.InArray(imap.FlagDeleted, flags.Flags) && flags.Op == imap.StoreFlagsAdd { + for _, data := range emailList { + s.deleteUidList = append(s.deleteUidList, data.UeId) + } + } + return nil } diff --git a/server/services/group/group.go b/server/services/group/group.go index 180b855..21e81c3 100644 --- a/server/services/group/group.go +++ b/server/services/group/group.go @@ -218,7 +218,8 @@ 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("email").Select("count(1)").Get(&value) + db.Instance.Table("user_email").Select("id").OrderBy("id desc").Get(&value) + value += 1 case "UIDVALIDITY": value = group.ID case "UNSEEN": @@ -241,7 +242,8 @@ func GetGroupStatus(ctx *context.Context, groupName string, params []string) (st case "MESSAGES": value = getGroupNum(ctx, groupName, false) case "UIDNEXT": - db.Instance.Table("email").Select("count(1)").Get(&value) + db.Instance.Table("user_email").Select("id").OrderBy("id desc").Get(&value) + value += 1 case "UIDVALIDITY": value = models.GroupNameToCode[groupName] case "UNSEEN": diff --git a/server/services/setup/ssl/ssl.go b/server/services/setup/ssl/ssl.go index 0108bd2..3de4d3c 100644 --- a/server/services/setup/ssl/ssl.go +++ b/server/services/setup/ssl/ssl.go @@ -290,7 +290,7 @@ func CheckSSLCrtInfo() (int, time.Time, bool, error) { nameMatchFail := true for _, name := range cert.DNSNames { - if strings.Contains("imap", name) { + if strings.Contains(name, "imap") { nameMatchFail = false break } diff --git a/server/services/setup/ssl/ssl_test.go b/server/services/setup/ssl/ssl_test.go index eb38881..d5132fd 100644 --- a/server/services/setup/ssl/ssl_test.go +++ b/server/services/setup/ssl/ssl_test.go @@ -9,7 +9,7 @@ import ( func TestCheckSSLCrtInfo(t *testing.T) { config.Init() - got, got1, _, err := CheckSSLCrtInfo() + got, got1, match, err := CheckSSLCrtInfo() - fmt.Println(got, got1, err) + fmt.Println(got, got1, match, err) } diff --git a/server/utils/goimap/action.go b/server/utils/goimap/action.go deleted file mode 100644 index 60a53e2..0000000 --- a/server/utils/goimap/action.go +++ /dev/null @@ -1,59 +0,0 @@ -package goimap - -type Action interface { - Create(session *Session, path string) CommandResponse // 创建邮箱 - Delete(session *Session, path string) CommandResponse // 删除邮箱 - Rename(session *Session, oldPath, newPath string) CommandResponse // 重命名邮箱 - List(session *Session, basePath, template string) CommandResponse // 浏览邮箱 - Append(session *Session, item string) CommandResponse // 上传邮件 - Select(session *Session, path string) CommandResponse // 选择邮箱 - /* - 读取邮件的文本信息,且仅用于显示的目的。 - ALL:只返回按照一定格式的邮件摘要,包括邮件标志、RFC822.SIZE、自身的时间和信封信息。IMAP客户机能够将标准邮件解析成这些信息并显示出来。 - BODY:只返回邮件体文本格式和大小的摘要信息。IMAP客户机可以识别这些细腻,并向用户显示详细的关于邮件的信息。其实是一些非扩展的BODYSTRUCTURE的信息。 - FAST:只返回邮件的一些摘要,包括邮件标志、RFC822.SIZE、和自身的时间。 - FULL:同样的还是一些摘要信息,包括邮件标志、RFC822.SIZE、自身的时间和BODYSTRUCTURE的信息。 - BODYSTRUCTUR:是邮件的[MIME-IMB]的体结构。这是服务器通过解析[RFC-2822]头中的[MIME-IMB]各字段和[MIME-IMB]头信息得出来的。包括的内容有:邮件正文的类型、字符集、编码方式等和各附件的类型、字符集、编码方式、文件名称等等。 - ENVELOPE:信息的信封结构。是服务器通过解析[RFC-2822]头中的[MIME-IMB]各字段得出来的,默认各字段都是需要的。主要包括:自身的时间、附件数、收件人、发件人等。 - FLAGS:此邮件的标志。 - INTERNALDATE:自身的时间。 - RFC822.SIZE:邮件的[RFC-2822]大小 - RFC822.HEADER:在功能上等同于BODY.PEEK[HEADER], - RFC822:功能上等同于BODY[]。 - RFC822.TEXT:功能上等同于BODY[TEXT] - UID:返回邮件的UID号,UID号是唯一标识邮件的一个号码。 - BODY[section] <>:返回邮件的中的某一指定部分,返回的部分用section来表示,section部分包含的信息通常是代表某一部分的一个数字或者是下面的某一个部分:HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, and TEXT。如果section部分是空的话,那就代表返回全部的信息,包括头信息。 - BODY[HEADER]返回完整的文件头信息。 - BODY[HEADER.FIELDS ()]:在小括号里面可以指定返回的特定字段。 - BODY[HEADER.FIELDS.NOT ()]:在小括号里面可以指定不需要返回的特定字段。 - BODY[MIME]:返回邮件的[MIME-IMB]的头信息,在正常情况下跟BODY[HEADER]没有区别。 - BODY[TEXT]:返回整个邮件体,这里的邮件体并不包括邮件头。 - **/ - Fetch(session *Session, mailIds, dataNames string, uid bool) CommandResponse - Store(session *Session, mailId, flags string, uid bool) CommandResponse // STORE 命令用于修改指定邮件的属性,包括给邮件打上已读标记、删除标记 - Close(session *Session) CommandResponse // 关闭文件夹 - Expunge(session *Session) CommandResponse // 删除已经标记为删除的邮件,释放服务器上的存储空间 - Examine(session *Session, path string) CommandResponse // 只读方式打开邮箱 - Subscribe(session *Session, path string) CommandResponse // 活动邮箱列表中增加一个邮箱 - UnSubscribe(session *Session, path string) CommandResponse // 活动邮箱列表中去掉一个邮箱 - LSub(session *Session, path, mailbox string) CommandResponse // 显示那些使用SUBSCRIBE命令设置为活动邮箱的文件 - /* - @category: - MESSAGES 邮箱中的邮件总数 - RECENT 邮箱中标志为\RECENT的邮件数 - UIDNEXT 可以分配给新邮件的下一个UID - UIDVALIDITY 邮箱的UID有效性标志 - UNSEEN 邮箱中没有被标志为\UNSEEN的邮件数 - */ - Status(session *Session, mailbox string, category []string) CommandResponse // 查询邮箱的当前状态 - Check(session *Session) CommandResponse // sync数据 - Search(session *Session, keyword, criteria string, uid bool) CommandResponse // 命令可以根据搜索条件在处于活动状态的邮箱中搜索邮件,然后显示匹配的邮件编号 - Copy(session *Session, mailId, mailBoxName string) CommandResponse // 把邮件从一个邮箱复制到另一个邮箱 - CapaBility(session *Session) CommandResponse // 返回IMAP服务器支持的功能列表 - Noop(session *Session) CommandResponse // 什么都不做,连接保活 - Login(session *Session, username, password string) CommandResponse // 登录 - Logout(session *Session) CommandResponse // 注销登录 - IDLE(session *Session) CommandResponse // 进入IDLE状态 - Unselect(session *Session) CommandResponse // 取消邮箱选择 - Custom(session *Session, cmd string, args string) CommandResponse -} diff --git a/server/utils/goimap/dto.go b/server/utils/goimap/dto.go deleted file mode 100644 index 9cb3f42..0000000 --- a/server/utils/goimap/dto.go +++ /dev/null @@ -1,15 +0,0 @@ -package goimap - -type CommandResponseType uint8 - -const ( - SUCCESS CommandResponseType = 0 - BAD CommandResponseType = 1 - NO CommandResponseType = 2 -) - -type CommandResponse struct { - Type CommandResponseType - Message string - Data []string -} diff --git a/server/utils/goimap/imap.go b/server/utils/goimap/imap.go deleted file mode 100644 index 2eb98fe..0000000 --- a/server/utils/goimap/imap.go +++ /dev/null @@ -1,830 +0,0 @@ -package goimap - -import ( - "bufio" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" - "github.com/Jinnrry/pmail/utils/context" - "github.com/Jinnrry/pmail/utils/id" - log "github.com/sirupsen/logrus" - "net" - "strings" - "sync" - "time" -) - -var ( - eol = "\r\n" -) - -// Server Imap服务实例 -type Server struct { - Domain string // 域名 - Port int // 端口 - TlsEnabled bool //是否启用Tls - TlsConfig *tls.Config // tls配置 - ConnectAliveTime time.Duration // 连接存活时间,默认不超时 - Action Action - stop chan bool - close bool - lck sync.Mutex -} - -// NewImapServer 新建一个服务实例 -func NewImapServer(port int, domain string, tlsEnabled bool, tlsConfig *tls.Config, action Action) *Server { - return &Server{ - Domain: domain, - Port: port, - TlsEnabled: tlsEnabled, - TlsConfig: tlsConfig, - Action: action, - stop: make(chan bool, 1), - } -} - -// Start 启动服务 -func (s *Server) Start() error { - if !s.TlsEnabled { - return s.startWithoutTLS() - } else { - return s.startWithTLS() - } -} - -func (s *Server) startWithTLS() error { - if s.lck.TryLock() { - listener, err := tls.Listen("tcp", fmt.Sprintf(":%d", s.Port), s.TlsConfig) - if err != nil { - return err - } - s.close = false - defer func() { - listener.Close() - }() - - go func() { - for { - conn, err := listener.Accept() - if err != nil { - if s.close { - break - } else { - continue - } - } - go s.handleClient(conn) - } - }() - <-s.stop - } else { - return errors.New("Server Is Running") - } - - return nil -} - -func (s *Server) startWithoutTLS() error { - if s.lck.TryLock() { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port)) - if err != nil { - return err - } - s.close = false - defer func() { - listener.Close() - }() - - go func() { - for { - conn, err := listener.Accept() - if err != nil { - if s.close { - break - } else { - continue - } - } - go s.handleClient(conn) - } - }() - <-s.stop - } else { - return errors.New("Server Is Running") - } - - return nil -} - -// Stop 停止服务 -func (s *Server) Stop() { - s.close = true - s.stop <- true -} - -func (s *Server) authenticate(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if args == "LOGIN" { - write(session, "+ VXNlciBOYW1lAA=="+eol, "") - line, err2 := reader.ReadString('\n') - if err2 != nil { - if conn != nil { - _ = conn.Close() - } - session.Conn = nil - session.IN_IDLE = false - return - } - account, err := base64.StdEncoding.DecodeString(line) - if err != nil { - showBad(session, "Data Error.", nub) - return - } - write(session, "+ UGFzc3dvcmQA"+eol, "") - line, err = reader.ReadString('\n') - if err2 != nil { - if conn != nil { - _ = conn.Close() - } - session.Conn = nil - session.IN_IDLE = false - return - } - password, err := base64.StdEncoding.DecodeString(line) - res := s.Action.Login(session, string(account), string(password)) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } else { - showBad(session, "Unsupported AUTHENTICATE mechanism.", nub) - } -} - -func (s *Server) capability(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - res := s.Action.CapaBility(session) - if res.Type == BAD { - write(session, fmt.Sprintf("* BAD %s%s", res.Message, eol), nub) - } else { - ret := "*" - for _, command := range res.Data { - ret += " " + command - } - ret += eol - write(session, ret, nub) - showSucc(session, res.Message, nub) - } -} - -func (s *Server) create(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "CREATE", nub) - return - } - res := s.Action.Create(session, args) - showSucc(session, res.Message, nub) -} - -func (s *Server) delete(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "DELETE", nub) - return - } - res := s.Action.Delete(session, args) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) rename(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "RENAME", nub) - } else { - dt := strings.Split(args, " ") - res := s.Action.Rename(session, dt[0], dt[1]) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) list(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "LIST", nub) - } else { - dt := strings.Split(args, " ") - dt[0] = strings.Trim(dt[0], `"`) - dt[1] = strings.Trim(dt[1], `"`) - res := s.Action.List(session, dt[0], dt[1]) - if res.Type == SUCCESS { - showSuccWithData(session, res.Data, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) append(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - log.WithContext(session.Ctx).Debugf("Append: %+v", args) -} - -func (s *Server) cselect(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - res := s.Action.Select(session, args) - if res.Type == SUCCESS { - showSuccWithData(session, res.Data, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) fetch(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader, uid bool) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "FETCH", nub) - } else { - dt := strings.SplitN(args, " ", 2) - if len(dt) != 2 { - showBad(session, "Error Params", nub) - return - } - - res := s.Action.Fetch(session, dt[0], dt[1], uid) - if res.Type == SUCCESS { - showSuccWithData(session, res.Data, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) store(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader, uid bool) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "RENAME", nub) - } else { - dt := strings.SplitN(args, " ", 2) - res := s.Action.Store(session, dt[0], dt[1], uid) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) cclose(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - res := s.Action.Close(session) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) expunge(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - res := s.Action.Expunge(session) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) examine(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "EXAMINE", nub) - } - res := s.Action.Examine(session, args) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) unsubscribe(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "UNSUBSCRIBE", nub) - } else { - res := s.Action.UnSubscribe(session, args) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) lsub(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "LSUB", nub) - } else { - dt := strings.Split(args, " ") - dt[0] = strings.Trim(dt[0], `"`) - res := s.Action.LSub(session, dt[0], dt[1]) - if res.Type == SUCCESS { - showSuccWithData(session, res.Data, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) status(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "STATUS", nub) - } else { - var mailBox string - var params []string - if strings.HasPrefix(args, `"`) { - dt := strings.Split(args, `"`) - if len(dt) >= 3 { - mailBox = dt[1] - } - dt[2] = strings.Trim(dt[2], "() ") - params = strings.Split(dt[2], " ") - } else { - dt := strings.SplitN(args, " ", 2) - dt[0] = strings.ReplaceAll(dt[0], `"`, "") - dt[1] = strings.Trim(dt[1], "()") - mailBox = dt[0] - params = strings.Split(dt[1], " ") - } - - res := s.Action.Status(session, mailBox, params) - if res.Type == SUCCESS { - showSuccWithData(session, res.Data, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) check(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - res := s.Action.Check(session) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) search(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader, uid bool) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "SEARCH", nub) - } else { - var res CommandResponse - if args == "ALL" { - res = s.Action.Search(session, "", "UID 1:*", uid) - } else { - res = s.Action.Search(session, "", args, uid) - } - - if res.Type == SUCCESS { - content := "* SEARCH" - for _, datum := range res.Data { - content += " " + datum - } - content += eol - content += fmt.Sprintf("%s OK SEARCH completed (Success)%s", nub, eol) - write(session, content, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) copy(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "COPY", nub) - } else { - dt := strings.SplitN(args, " ", 2) - res := s.Action.Copy(session, dt[0], dt[1]) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) noop(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - res := s.Action.Noop(session) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) login(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if args == "" { - paramsErr(session, "LOGIN", nub) - } else { - dt := strings.SplitN(args, " ", 2) - res := s.Action.Login(session, strings.Trim(dt[0], `"`), strings.Trim(dt[1], `"`)) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) logout(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - res := s.Action.Logout(session) - write(session, "* BYE PMail Server logging out"+eol, nub) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - if conn != nil { - _ = conn.Close() - } -} - -func (s *Server) unselect(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - res := s.Action.Unselect(session) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) subscribe(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - if args == "" { - paramsErr(session, "SUBSCRIBE", nub) - } else { - res := s.Action.Subscribe(session, args) - if res.Type == SUCCESS { - showSucc(session, res.Message, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } - } -} - -func (s *Server) idle(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) { - if session.Status != AUTHORIZED { - showBad(session, "Need Login", nub) - return - } - session.IN_IDLE = true - res := s.Action.IDLE(session) - if res.Type == SUCCESS { - write(session, "+ idling"+eol, nub) - } else if res.Type == BAD { - showBad(session, res.Message, nub) - } else { - showNo(session, res.Message, nub) - } -} - -func (s *Server) custom(session *Session, cmd string, args string, nub string, conn net.Conn, reader *bufio.Reader) { - res := s.Action.Custom(session, cmd, args) - if res.Type == BAD { - write(session, fmt.Sprintf("* BAD %s %s", res.Message, eol), nub) - } else if res.Type == NO { - showNo(session, res.Message, nub) - } else { - if len(res.Data) == 0 { - showSucc(session, res.Message, nub) - } else { - ret := "" - for _, re := range res.Data { - ret += fmt.Sprintf("%s%s", re, eol) - } - ret += "." + eol - write(session, fmt.Sprintf(ret), nub) - } - } -} - -func (s *Server) doCommand(session *Session, rawLine string, conn net.Conn, reader *bufio.Reader) { - nub, cmd, args := getCommand(rawLine) - log.WithContext(session.Ctx).Debugf("Imap Input:\t %s", rawLine) - if cmd != "IDLE" { - session.IN_IDLE = false - } - - switch cmd { - case "": - if conn != nil { - conn.Close() - conn = nil - } - break - - case "AUTHENTICATE": - s.authenticate(session, args, nub, conn, reader) - case "CAPABILITY": - s.capability(session, rawLine, nub, conn, reader) - case "CREATE": - s.create(session, args, nub, conn, reader) - case "DELETE": - s.delete(session, args, nub, conn, reader) - case "RENAME": - s.rename(session, args, nub, conn, reader) - case "LIST": - s.list(session, args, nub, conn, reader) - case "APPEND": - s.append(session, args, nub, conn, reader) - case "SELECT": - s.cselect(session, args, nub, conn, reader) - case "FETCH": - s.fetch(session, args, nub, conn, reader, false) - case "UID FETCH": - s.fetch(session, args, nub, conn, reader, true) - case "STORE": - s.store(session, args, nub, conn, reader, false) - case "UID STORE": - s.store(session, args, nub, conn, reader, true) - case "CLOSE": - s.cclose(session, args, nub, conn, reader) - case "EXPUNGE": - s.expunge(session, args, nub, conn, reader) - case "EXAMINE": - s.examine(session, args, nub, conn, reader) - case "SUBSCRIBE": - s.subscribe(session, args, nub, conn, reader) - case "UNSUBSCRIBE": - s.unsubscribe(session, args, nub, conn, reader) - case "LSUB": - s.lsub(session, args, nub, conn, reader) - case "STATUS": - s.status(session, args, nub, conn, reader) - case "CHECK": - s.check(session, args, nub, conn, reader) - case "SEARCH": - s.search(session, args, nub, conn, reader, false) - case "UID SEARCH": - s.search(session, args, nub, conn, reader, true) - case "COPY": - s.copy(session, args, nub, conn, reader) - case "NOOP": - s.noop(session, args, nub, conn, reader) - case "LOGIN": - s.login(session, args, nub, conn, reader) - case "LOGOUT": - s.logout(session, args, nub, conn, reader) - case "UNSELECT": - s.unselect(session, args, nub, conn, reader) - case "IDLE": - s.idle(session, args, nub, conn, reader) - default: - s.custom(session, cmd, args, nub, conn, reader) - } -} - -func (s *Server) handleClient(conn net.Conn) { - - defer func() { - if conn != nil { - _ = conn.Close() - } - }() - - session := &Session{ - Conn: conn, - Status: UNAUTHORIZED, - AliveTime: time.Now(), - } - - tc := &context.Context{} - tc.SetValue(context.LogID, id.GenLogID()) - session.Ctx = tc - - if s.TlsEnabled && s.TlsConfig != nil { - session.InTls = true - } - - // 检查连接是否超时 - if s.ConnectAliveTime != 0 { - go func() { - for { - if time.Now().Sub(session.AliveTime) >= s.ConnectAliveTime { - if session.Conn != nil { - write(session, "* BYE AutoLogout; idle for too long", "") - _ = session.Conn.Close() - } - session.Conn = nil - session.IN_IDLE = false - return - } - time.Sleep(3 * time.Second) - } - }() - } - - reader := bufio.NewReader(conn) - write(session, fmt.Sprintf(`* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN] PMail Server ready%s`, eol), "") - - for { - rawLine, err := reader.ReadString('\n') - if err != nil { - if conn != nil { - _ = conn.Close() - } - session.Conn = nil - session.IN_IDLE = false - return - } - session.AliveTime = time.Now() - - s.doCommand(session, rawLine, conn, reader) - - } -} - -// cuts the line into command and arguments -func getCommand(line string) (string, string, string) { - line = strings.Trim(line, "\r \n") - cmd := strings.SplitN(line, " ", 3) - if len(cmd) == 1 { - return "", "", "" - } - - if len(cmd) == 3 { - if strings.ToTitle(cmd[1]) == "UID" { - args := strings.SplitN(cmd[2], " ", 2) - if len(args) >= 2 { - return cmd[0], strings.ToTitle(cmd[1]) + " " + strings.ToTitle(args[0]), args[1] - } - } - - return cmd[0], strings.ToTitle(cmd[1]), cmd[2] - } - - return cmd[0], strings.ToTitle(cmd[1]), "" -} - -func getSafeArg(args []string, nr int) string { - if nr < len(args) { - return args[nr] - } - return "" -} - -func showSucc(s *Session, msg, nub string) { - if msg == "" { - write(s, fmt.Sprintf("%s OK success %s", nub, eol), nub) - } else { - write(s, fmt.Sprintf("%s %s %s", nub, msg, eol), nub) - } -} - -func showSuccWithData(s *Session, data []string, msg string, nub string) { - content := "" - for _, datum := range data { - content += fmt.Sprintf("%s%s", datum, eol) - } - content += fmt.Sprintf("%s OK %s%s", nub, msg, eol) - write(s, content, nub) -} - -func showBad(s *Session, err string, nub string) { - if nub == "" { - nub = "*" - } - - if err == "" { - write(s, fmt.Sprintf("%s BAD %s", nub, eol), nub) - return - } - write(s, fmt.Sprintf("%s BAD %s%s", nub, err, eol), nub) -} - -func showNo(s *Session, msg string, nub string) { - write(s, fmt.Sprintf("%s NO %s%s", nub, msg, eol), nub) -} - -func paramsErr(session *Session, commend string, nub string) { - write(session, fmt.Sprintf("* BAD %s parameters! %s", commend, eol), nub) -} - -func write(session *Session, content string, nub string) { - if !strings.HasSuffix(content, eol) { - log.WithContext(session.Ctx).Errorf("Error:返回结尾错误 %s", content) - } - log.WithContext(session.Ctx).Debugf("Imap Out:\t |%s", content) - fmt.Fprintf(session.Conn, content) -} diff --git a/server/utils/goimap/imap_test.go b/server/utils/goimap/imap_test.go deleted file mode 100644 index 1603c21..0000000 --- a/server/utils/goimap/imap_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package goimap - -import ( - "fmt" - "net" - "net/netip" - "reflect" - "testing" - "time" -) - -func Test_paramsErr(t *testing.T) { - -} - -func Test_getCommand(t *testing.T) { - type args struct { - line string - } - tests := []struct { - name string - args args - want string - want1 string - want2 string - }{ - { - "STATUS命令测试", - args{`15.64 STATUS "Deleted Messages" (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`}, - "15.64", - "STATUS", - `"Deleted Messages" (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`, - }, - { - "LOGIN命令测试", - args{`a LOGIN admin 666666`}, - "a", - "LOGIN", - `admin 666666`, - }, - { - "SELECT命令测试", - args{`9.79 SELECT INBOX`}, - "9.79", - "SELECT", - `INBOX`, - }, - { - "CAPABILITY命令测试", - args{`1.81 CAPABILITY`}, - "1.81", - "CAPABILITY", - ``, - }, - { - "DELETE命令测试", - args{`3.183 SELECT "Deleted Messages"`}, - "3.183", - "SELECT", - `"Deleted Messages"`, - }, - { - "异常命令测试", - args{`GET/HTTP/1.0`}, - "", - "", - ``, - }, - { - "FETCH命令测试", - args{`4.189 FETCH 7:38 (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from to cc message-id in-reply-to references content-type x-priority x-uniform-type-identifier x-universally-unique-identifier list-id list-unsubscribe bimi-indicator bimi-location x-bimi-indicator-hash authentication-results dkim-signature)])`}, - "4.189", - "FETCH", - `7:38 (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from to cc message-id in-reply-to references content-type x-priority x-uniform-type-identifier x-universally-unique-identifier list-id list-unsubscribe bimi-indicator bimi-location x-bimi-indicator-hash authentication-results dkim-signature)])`, - }, - { - "FETCH命令测试2", - args{`4.167 FETCH 1:41 (FLAGS UID)`}, - "4.167", - "FETCH", - `1:41 (FLAGS UID)`, - }, - { - "UID FETCH命令测试", - args{`4.200 UID FETCH 5 BODY.PEEK[HEADER]`}, - "4.200", - "UID FETCH", - `5 BODY.PEEK[HEADER]`, - }, - { - "UID Search命令测试", - args{`C117 UID SEARCH UID 46:*`}, - "C117", - "UID SEARCH", - `UID 46:*`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, got1, got2 := getCommand(tt.args.line) - if got != tt.want { - t.Errorf("getCommand() got = %v, want %v", got, tt.want) - } - if got1 != tt.want1 { - t.Errorf("getCommand() got1 = %v, want %v", got1, tt.want1) - } - if !reflect.DeepEqual(got2, tt.want2) { - t.Errorf("getCommand() got2 = %v, want %v", got2, tt.want2) - } - }) - } -} - -type mockConn struct{} - -func (m mockConn) Read(b []byte) (n int, err error) { - fmt.Println("Read") - return 0, err -} - -func (m mockConn) Write(b []byte) (n int, err error) { - return 0, err -} - -func (m mockConn) Close() error { - return nil -} - -func (m mockConn) LocalAddr() net.Addr { - return net.TCPAddrFromAddrPort(netip.AddrPort{}) -} - -func (m mockConn) RemoteAddr() net.Addr { - return net.TCPAddrFromAddrPort(netip.AddrPort{}) -} - -func (m mockConn) SetDeadline(t time.Time) error { - return nil -} - -func (m mockConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (m mockConn) SetWriteDeadline(t time.Time) error { - return nil -} - -// -//func TestServer_doCommand(t *testing.T) { -// type args struct { -// session *Session -// rawLine string -// conn net.Conn -// reader *bufio.Reader -// } -// tests := []struct { -// name string -// args args -// }{ -// { -// name: "StatusTest", -// args: args{ -// session: &Session{ -// Status: AUTHORIZED, -// -// }, -// rawLine: `9.33 STATUS "Sent Messages" (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`, -// conn: &mockConn{}, -// reader: &bufio.Reader{}, -// }, -// }, -// { -// name: "StatusTest2", -// args: args{ -// session: &Session{ -// Status: AUTHORIZED, -// }, -// rawLine: `9.33 STATUS INBOX (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`, -// conn: &mockConn{}, -// reader: &bufio.Reader{}, -// }, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// s := &Server{ -// } -// s.doCommand(tt.args.session, tt.args.rawLine, tt.args.conn, tt.args.reader) -// }) -// } -//} diff --git a/server/utils/goimap/session.go b/server/utils/goimap/session.go deleted file mode 100644 index e54c1f6..0000000 --- a/server/utils/goimap/session.go +++ /dev/null @@ -1,28 +0,0 @@ -package goimap - -import ( - "context" - "net" - "time" -) - -type Status int8 - -const ( - UNAUTHORIZED Status = 1 - AUTHORIZED Status = 2 - SELECTED Status = 3 - LOGOUT Status = 4 -) - -type Session struct { - Status Status - Account string - DeleteIds []int64 - Ctx context.Context - Conn net.Conn - InTls bool - AliveTime time.Time - CurrentPath string //当前选择的文件夹 - IN_IDLE bool // 是否处在IDLE中 -}