feature/v2.8.1 (#239)

修复ssl证书校验失败
部分客户端兼容性修复
修复邮件移动失败问题
支持“广告箱”
This commit is contained in:
Jinnrry 2025-01-06 21:01:38 +08:00 committed by GitHub
parent 5af46b32f6
commit 69800a8a0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 73 additions and 1166 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,6 @@ func StarTLS() {
},
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
},
TLSConfig: tlsConfig,
InsecureAuth: false,

View File

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

View File

@ -38,6 +38,7 @@ type serverSession struct {
status Status
currentMailbox string
connectTime time.Time
deleteUidList []int
}
// NewSession creates a new IMAP session.

View File

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

View File

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

View File

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

View File

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

View File

@ -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":

View File

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

View File

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

View File

@ -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] <<partial>>返回邮件的中的某一指定部分返回的部分用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
}

View File

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

View File

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

View File

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

View File

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