|
|
@@ -0,0 +1,544 @@
|
|
|
+package goimap
|
|
|
+
|
|
|
+import (
|
|
|
+ "bufio"
|
|
|
+ "crypto/tls"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ log "github.com/sirupsen/logrus"
|
|
|
+ "io"
|
|
|
+ "log/slog"
|
|
|
+ "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) handleClient(conn net.Conn) {
|
|
|
+ slog.Debug("Imap conn")
|
|
|
+
|
|
|
+ defer func() {
|
|
|
+ if conn != nil {
|
|
|
+ _ = conn.Close()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ session := &Session{
|
|
|
+ Conn: conn,
|
|
|
+ Status: UNAUTHORIZED,
|
|
|
+ AliveTime: time.Now(),
|
|
|
+ }
|
|
|
+ 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.Conn, "* 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(conn, `* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN] PMail Server ready`, "")
|
|
|
+
|
|
|
+ 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()
|
|
|
+
|
|
|
+ nub, cmd, args := getCommand(rawLine)
|
|
|
+ log.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 "CAPABILITY":
|
|
|
+ commands, err := s.Action.CapaBility(session)
|
|
|
+ if err != nil {
|
|
|
+ write(conn, fmt.Sprintf("* BAD %s%s", err.Error(), eol), nub)
|
|
|
+ } else {
|
|
|
+ ret := "*"
|
|
|
+ for _, command := range commands {
|
|
|
+ ret += " " + command
|
|
|
+ }
|
|
|
+ write(conn, ret, nub)
|
|
|
+ showSucc(conn, nub)
|
|
|
+ }
|
|
|
+
|
|
|
+ case "CREATE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "CREATE", nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ err := s.Action.Create(session, args)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "DELETE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "DELETE", nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ err := s.Action.Delete(session, args)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "RENAME":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "RENAME", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.Split(args, " ")
|
|
|
+ err := s.Action.Rename(session, dt[0], dt[1])
|
|
|
+ output(conn, nub, err)
|
|
|
+ }
|
|
|
+ case "LIST":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "LIST", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.Split(args, " ")
|
|
|
+ dt[0] = strings.ReplaceAll(dt[0], `"`, "")
|
|
|
+ rets, err := s.Action.List(session, dt[0], dt[1])
|
|
|
+ if err != nil {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ } else {
|
|
|
+ ret := ""
|
|
|
+ for _, str := range rets {
|
|
|
+ ret += str + eol
|
|
|
+ }
|
|
|
+ write(conn, ret, nub)
|
|
|
+ showSucc(conn, nub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "APPEND":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ log.Debugf("Append: %+v", args)
|
|
|
+ case "SELECT":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ ret, err := s.Action.Select(session, args)
|
|
|
+ args = strings.ReplaceAll(args, `"`, "")
|
|
|
+ if err != nil {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ } else {
|
|
|
+ for _, s2 := range ret {
|
|
|
+ write(conn, s2, nub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ case "FETCH":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "RENAME", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.Split(args, " ")
|
|
|
+ ret, err := s.Action.Fetch(session, dt[0], dt[1])
|
|
|
+ if err != nil {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ } else {
|
|
|
+ write(conn, ret, nub)
|
|
|
+ showSucc(conn, ret)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "STORE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "RENAME", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.Split(args, " ")
|
|
|
+ err := s.Action.Store(session, dt[0], dt[1])
|
|
|
+ output(conn, nub, err)
|
|
|
+ }
|
|
|
+ case "CLOSE":
|
|
|
+ err := s.Action.Close(session)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "EXPUNGE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ err := s.Action.Expunge(session)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "EXAMINE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "EXAMINE", nub)
|
|
|
+ }
|
|
|
+ err := s.Action.Examine(session, args)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "SUBSCRIBE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "SUBSCRIBE", nub)
|
|
|
+ } else {
|
|
|
+ err := s.Action.Subscribe(session, args)
|
|
|
+ output(conn, nub, err)
|
|
|
+ }
|
|
|
+ case "UNSUBSCRIBE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "UNSUBSCRIBE", nub)
|
|
|
+ } else {
|
|
|
+ err := s.Action.UnSubscribe(session, args)
|
|
|
+ output(conn, nub, err)
|
|
|
+ }
|
|
|
+ case "LSUB":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "LSUB", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.Split(args, " ")
|
|
|
+ rets, err := s.Action.LSub(session, dt[0], dt[1])
|
|
|
+ if err != nil {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ } else {
|
|
|
+ ret := ""
|
|
|
+ for _, str := range rets {
|
|
|
+ ret += str + eol
|
|
|
+ }
|
|
|
+ write(conn, ret, nub)
|
|
|
+ showSucc(conn, nub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "STATUS":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "STATUS", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.SplitN(args, " ", 2)
|
|
|
+ dt[0] = strings.ReplaceAll(dt[0], `"`, "")
|
|
|
+ dt[1] = strings.Trim(dt[1], "()")
|
|
|
+ params := strings.Split(dt[1], " ")
|
|
|
+
|
|
|
+ ret, err := s.Action.Status(session, dt[0], params)
|
|
|
+ if err != nil {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ } else {
|
|
|
+ write(conn, ret, nub)
|
|
|
+ showSucc(conn, nub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "CHECK":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ err := s.Action.Check(session)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "SEARCH":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "SEARCH", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.SplitN(args, " ", 2)
|
|
|
+ ret, err := s.Action.Search(session, dt[0], dt[1])
|
|
|
+ if err != nil {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ } else {
|
|
|
+ write(conn, ret, nub)
|
|
|
+ showSucc(conn, nub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "COPY":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "COPY", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.SplitN(args, " ", 2)
|
|
|
+ err := s.Action.Copy(session, dt[0], dt[1])
|
|
|
+ output(conn, nub, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ case "NOOP":
|
|
|
+ err := s.Action.Noop(session)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "LOGIN":
|
|
|
+ if args == "" {
|
|
|
+ paramsErr(conn, "LOGIN", nub)
|
|
|
+ } else {
|
|
|
+ dt := strings.SplitN(args, " ", 2)
|
|
|
+ err := s.Action.Login(session, dt[0], dt[1])
|
|
|
+ output(conn, nub, err)
|
|
|
+ }
|
|
|
+ case "LOGOUT":
|
|
|
+ err := s.Action.Logout(session)
|
|
|
+ write(conn, "* BYE PMail Server logging out", nub)
|
|
|
+ output(conn, nub, err)
|
|
|
+ if conn != nil {
|
|
|
+ _ = conn.Close()
|
|
|
+ }
|
|
|
+ case "UNSELECT":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ err := s.Action.Unselect(session)
|
|
|
+ output(conn, nub, err)
|
|
|
+ case "IDLE":
|
|
|
+ if session.Status != AUTHORIZED {
|
|
|
+ showBad(conn, errors.New("Need Login"), nub)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ session.IN_IDLE = true
|
|
|
+ err := s.Action.IDLE(session)
|
|
|
+ if err != nil {
|
|
|
+ write(conn, fmt.Sprintf("+ idling%s", eol), nub)
|
|
|
+ } else {
|
|
|
+ showBad(conn, err, nub)
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ rets, err := s.Action.Custom(session, cmd, args)
|
|
|
+ if err != nil {
|
|
|
+ write(conn, fmt.Sprintf("* BAD %s %s", err.Error(), eol), nub)
|
|
|
+ } else {
|
|
|
+ if len(rets) == 0 {
|
|
|
+ write(conn, fmt.Sprintf("%s OK %s", nub, eol), nub)
|
|
|
+ } else if len(rets) == 1 {
|
|
|
+ write(conn, fmt.Sprintf("%s OK %s%s", nub, rets[0], eol), nub)
|
|
|
+ } else {
|
|
|
+ ret := fmt.Sprintf("%s OK %s", nub, eol)
|
|
|
+ for _, re := range rets {
|
|
|
+ ret += fmt.Sprintf("%s%s", re, eol)
|
|
|
+ }
|
|
|
+ ret += "." + eol
|
|
|
+ write(conn, fmt.Sprintf(ret), nub)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 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 "", "", ""
|
|
|
+ }
|
|
|
+
|
|
|
+ for i, s := range cmd {
|
|
|
+ cmd[i] = s
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(cmd) == 3 {
|
|
|
+ return strings.ToTitle(cmd[0]), strings.ToTitle(cmd[1]), cmd[2]
|
|
|
+ }
|
|
|
+
|
|
|
+ return strings.ToTitle(cmd[0]), strings.ToTitle(cmd[1]), ""
|
|
|
+}
|
|
|
+
|
|
|
+func getSafeArg(args []string, nr int) string {
|
|
|
+ if nr < len(args) {
|
|
|
+ return args[nr]
|
|
|
+ }
|
|
|
+ return ""
|
|
|
+}
|
|
|
+
|
|
|
+func showSucc(w io.Writer, nub string) {
|
|
|
+ write(w, fmt.Sprintf("%s OK success %s", nub, eol), nub)
|
|
|
+}
|
|
|
+
|
|
|
+func showBad(w io.Writer, err error, nub string) {
|
|
|
+ if err == nil {
|
|
|
+ write(w, fmt.Sprintf("* BAD %s", eol), nub)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ write(w, fmt.Sprintf("* BAD %s%s", err.Error(), eol), nub)
|
|
|
+}
|
|
|
+
|
|
|
+func output(w io.Writer, nub string, err error) {
|
|
|
+ if err != nil {
|
|
|
+ showBad(w, err, nub)
|
|
|
+ } else {
|
|
|
+ showSucc(w, nub)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func paramsErr(w io.Writer, commend string, nub string) {
|
|
|
+ write(w, fmt.Sprintf("* BAD %s parameters! %s", commend, eol), nub)
|
|
|
+}
|
|
|
+
|
|
|
+func write(w io.Writer, content string, nub string) {
|
|
|
+ content = strings.ReplaceAll(content, "$$NUM", nub)
|
|
|
+ log.Debugf("Imap Out:\t |%s", content)
|
|
|
+ fmt.Fprintf(w, content)
|
|
|
+}
|