This is the v1 version, had the v2 before.
This commit is contained in:
421
server/conn.go
Normal file
421
server/conn.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/backend"
|
||||
)
|
||||
|
||||
// Conn is a connection to a client.
|
||||
type Conn interface {
|
||||
io.Reader
|
||||
|
||||
// Server returns this connection's server.
|
||||
Server() *Server
|
||||
// Context returns this connection's context.
|
||||
Context() *Context
|
||||
// Capabilities returns a list of capabilities enabled for this connection.
|
||||
Capabilities() []string
|
||||
// WriteResp writes a response to this connection.
|
||||
WriteResp(res imap.WriterTo) error
|
||||
// IsTLS returns true if TLS is enabled.
|
||||
IsTLS() bool
|
||||
// TLSState returns the TLS connection state if TLS is enabled, nil otherwise.
|
||||
TLSState() *tls.ConnectionState
|
||||
// Upgrade upgrades a connection, e.g. wrap an unencrypted connection with an
|
||||
// encrypted tunnel.
|
||||
Upgrade(upgrader imap.ConnUpgrader) error
|
||||
// Close closes this connection.
|
||||
Close() error
|
||||
WaitReady()
|
||||
|
||||
Info() *imap.ConnInfo
|
||||
|
||||
setTLSConn(*tls.Conn)
|
||||
silent() *bool // TODO: remove this
|
||||
serve(Conn) error
|
||||
commandHandler(cmd *imap.Command) (hdlr Handler, err error)
|
||||
}
|
||||
|
||||
// Context stores a connection's metadata.
|
||||
type Context struct {
|
||||
// This connection's current state.
|
||||
State imap.ConnState
|
||||
// If the client is logged in, the user.
|
||||
User backend.User
|
||||
// If the client has selected a mailbox, the mailbox.
|
||||
Mailbox backend.Mailbox
|
||||
// True if the currently selected mailbox has been opened in read-only mode.
|
||||
MailboxReadOnly bool
|
||||
// Responses to send to the client.
|
||||
Responses chan<- imap.WriterTo
|
||||
// Closed when the client is logged out.
|
||||
LoggedOut <-chan struct{}
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
*imap.Conn
|
||||
|
||||
conn Conn // With extensions overrides
|
||||
s *Server
|
||||
ctx *Context
|
||||
tlsConn *tls.Conn
|
||||
continues chan bool
|
||||
upgrade chan bool
|
||||
responses chan imap.WriterTo
|
||||
loggedOut chan struct{}
|
||||
silentVal bool
|
||||
}
|
||||
|
||||
func newConn(s *Server, c net.Conn) *conn {
|
||||
// Create an imap.Reader and an imap.Writer
|
||||
continues := make(chan bool)
|
||||
r := imap.NewServerReader(nil, continues)
|
||||
w := imap.NewWriter(nil)
|
||||
|
||||
responses := make(chan imap.WriterTo)
|
||||
loggedOut := make(chan struct{})
|
||||
|
||||
tlsConn, _ := c.(*tls.Conn)
|
||||
|
||||
conn := &conn{
|
||||
Conn: imap.NewConn(c, r, w),
|
||||
|
||||
s: s,
|
||||
ctx: &Context{
|
||||
State: imap.ConnectingState,
|
||||
Responses: responses,
|
||||
LoggedOut: loggedOut,
|
||||
},
|
||||
tlsConn: tlsConn,
|
||||
continues: continues,
|
||||
upgrade: make(chan bool),
|
||||
responses: responses,
|
||||
loggedOut: loggedOut,
|
||||
}
|
||||
|
||||
if s.Debug != nil {
|
||||
conn.Conn.SetDebug(s.Debug)
|
||||
}
|
||||
if s.MaxLiteralSize > 0 {
|
||||
conn.Conn.MaxLiteralSize = s.MaxLiteralSize
|
||||
}
|
||||
|
||||
go conn.send()
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (c *conn) Server() *Server {
|
||||
return c.s
|
||||
}
|
||||
|
||||
func (c *conn) Context() *Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
type response struct {
|
||||
response imap.WriterTo
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (r *response) WriteTo(w *imap.Writer) error {
|
||||
err := r.response.WriteTo(w)
|
||||
close(r.done)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) setDeadline() {
|
||||
if c.s.AutoLogout == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
dur := c.s.AutoLogout
|
||||
if dur < MinAutoLogout {
|
||||
dur = MinAutoLogout
|
||||
}
|
||||
t := time.Now().Add(dur)
|
||||
|
||||
c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *conn) WriteResp(r imap.WriterTo) error {
|
||||
done := make(chan struct{})
|
||||
c.responses <- &response{r, done}
|
||||
<-done
|
||||
c.setDeadline()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Close() error {
|
||||
if c.ctx.User != nil {
|
||||
c.ctx.User.Logout()
|
||||
}
|
||||
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *conn) Capabilities() []string {
|
||||
caps := []string{"IMAP4rev1", "LITERAL+", "SASL-IR", "CHILDREN", "UNSELECT", "MOVE", "IDLE"}
|
||||
|
||||
appendLimitSet := false
|
||||
if c.ctx.State == imap.AuthenticatedState {
|
||||
if u, ok := c.ctx.User.(backend.AppendLimitUser); ok {
|
||||
if limit := u.CreateMessageLimit(); limit != nil {
|
||||
caps = append(caps, fmt.Sprintf("APPENDLIMIT=%v", *limit))
|
||||
appendLimitSet = true
|
||||
}
|
||||
}
|
||||
} else if be, ok := c.Server().Backend.(backend.AppendLimitBackend); ok {
|
||||
if limit := be.CreateMessageLimit(); limit != nil {
|
||||
caps = append(caps, fmt.Sprintf("APPENDLIMIT=%v", *limit))
|
||||
appendLimitSet = true
|
||||
}
|
||||
}
|
||||
if !appendLimitSet {
|
||||
caps = append(caps, "APPENDLIMIT")
|
||||
}
|
||||
|
||||
if c.ctx.State == imap.NotAuthenticatedState {
|
||||
if !c.IsTLS() && c.s.TLSConfig != nil {
|
||||
caps = append(caps, "STARTTLS")
|
||||
}
|
||||
|
||||
if !c.canAuth() {
|
||||
caps = append(caps, "LOGINDISABLED")
|
||||
} else {
|
||||
for name := range c.s.auths {
|
||||
caps = append(caps, "AUTH="+name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range c.s.extensions {
|
||||
caps = append(caps, ext.Capabilities(c)...)
|
||||
}
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
func (c *conn) writeAndFlush(w imap.WriterTo) error {
|
||||
if err := w.WriteTo(c.Writer); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Writer.Flush()
|
||||
}
|
||||
|
||||
func (c *conn) send() {
|
||||
// Send responses
|
||||
for {
|
||||
select {
|
||||
case <-c.upgrade:
|
||||
// Wait until upgrade is finished.
|
||||
c.Wait()
|
||||
case needCont := <-c.continues:
|
||||
// Send continuation requests
|
||||
if needCont {
|
||||
resp := &imap.ContinuationReq{Info: "send literal"}
|
||||
if err := c.writeAndFlush(resp); err != nil {
|
||||
c.Server().ErrorLog.Println("cannot send continuation request: ", err)
|
||||
}
|
||||
}
|
||||
case res := <-c.responses:
|
||||
// Got a response that needs to be sent
|
||||
// Request to send the response
|
||||
if err := c.writeAndFlush(res); err != nil {
|
||||
c.Server().ErrorLog.Println("cannot send response: ", err)
|
||||
}
|
||||
case <-c.loggedOut:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) greet() error {
|
||||
c.ctx.State = imap.NotAuthenticatedState
|
||||
|
||||
caps := c.Capabilities()
|
||||
args := make([]interface{}, len(caps))
|
||||
for i, cap := range caps {
|
||||
args[i] = imap.RawString(cap)
|
||||
}
|
||||
|
||||
greeting := &imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Code: imap.CodeCapability,
|
||||
Arguments: args,
|
||||
Info: "IMAP4rev1 Service Ready",
|
||||
}
|
||||
|
||||
return c.WriteResp(greeting)
|
||||
}
|
||||
|
||||
func (c *conn) setTLSConn(tlsConn *tls.Conn) {
|
||||
c.tlsConn = tlsConn
|
||||
}
|
||||
|
||||
func (c *conn) IsTLS() bool {
|
||||
return c.tlsConn != nil
|
||||
}
|
||||
|
||||
func (c *conn) TLSState() *tls.ConnectionState {
|
||||
if c.tlsConn != nil {
|
||||
state := c.tlsConn.ConnectionState()
|
||||
return &state
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// canAuth checks if the client can use plain text authentication.
|
||||
func (c *conn) canAuth() bool {
|
||||
return c.IsTLS() || c.s.AllowInsecureAuth
|
||||
}
|
||||
|
||||
func (c *conn) silent() *bool {
|
||||
return &c.silentVal
|
||||
}
|
||||
|
||||
func (c *conn) serve(conn Conn) (err error) {
|
||||
c.conn = conn
|
||||
|
||||
defer func() {
|
||||
c.ctx.State = imap.LogoutState
|
||||
close(c.loggedOut)
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.WriteResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespBye,
|
||||
Info: "Internal server error, closing connection.",
|
||||
})
|
||||
|
||||
stack := debug.Stack()
|
||||
c.s.ErrorLog.Printf("panic serving %v: %v\n%s", c.Info().RemoteAddr, r, stack)
|
||||
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Send greeting
|
||||
if err := c.greet(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
if c.ctx.State == imap.LogoutState {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res *imap.StatusResp
|
||||
var up Upgrader
|
||||
|
||||
fields, err := c.ReadLine()
|
||||
if err == io.EOF || c.ctx.State == imap.LogoutState {
|
||||
return nil
|
||||
}
|
||||
c.setDeadline()
|
||||
|
||||
if err != nil {
|
||||
if imap.IsParseError(err) {
|
||||
res = &imap.StatusResp{
|
||||
Type: imap.StatusRespBad,
|
||||
Info: err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.s.ErrorLog.Println("cannot read command:", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cmd := &imap.Command{}
|
||||
if err := cmd.Parse(fields); err != nil {
|
||||
res = &imap.StatusResp{
|
||||
Tag: cmd.Tag,
|
||||
Type: imap.StatusRespBad,
|
||||
Info: err.Error(),
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
res, up, err = c.handleCommand(cmd)
|
||||
if err != nil {
|
||||
res = &imap.StatusResp{
|
||||
Tag: cmd.Tag,
|
||||
Type: imap.StatusRespBad,
|
||||
Info: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
|
||||
if err := c.WriteResp(res); err != nil {
|
||||
c.s.ErrorLog.Println("cannot write response:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if up != nil && res.Type == imap.StatusRespOk {
|
||||
if err := up.Upgrade(c.conn); err != nil {
|
||||
c.s.ErrorLog.Println("cannot upgrade connection:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) WaitReady() {
|
||||
c.upgrade <- true
|
||||
c.Conn.WaitReady()
|
||||
}
|
||||
|
||||
func (c *conn) commandHandler(cmd *imap.Command) (hdlr Handler, err error) {
|
||||
newHandler := c.s.Command(cmd.Name)
|
||||
if newHandler == nil {
|
||||
err = errors.New("Unknown command")
|
||||
return
|
||||
}
|
||||
|
||||
hdlr = newHandler()
|
||||
err = hdlr.Parse(cmd.Arguments)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrader, err error) {
|
||||
hdlr, err := c.commandHandler(cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hdlrErr := hdlr.Handle(c.conn)
|
||||
if statusErr, ok := hdlrErr.(*imap.ErrStatusResp); ok {
|
||||
res = statusErr.Resp
|
||||
} else if hdlrErr != nil {
|
||||
res = &imap.StatusResp{
|
||||
Type: imap.StatusRespNo,
|
||||
Info: hdlrErr.Error(),
|
||||
}
|
||||
} else {
|
||||
res = &imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
}
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
res.Tag = cmd.Tag
|
||||
|
||||
if res.Type == imap.StatusRespOk && res.Info == "" {
|
||||
res.Info = cmd.Name + " completed"
|
||||
}
|
||||
}
|
||||
|
||||
up, _ = hdlr.(Upgrader)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user