Forked the emersion/go-imap v1 project.

This commit is contained in:
2025-05-01 11:58:18 +03:00
commit bcc3f95e8e
107 changed files with 16268 additions and 0 deletions

617
imapserver/conn.go Normal file
View File

@@ -0,0 +1,617 @@
package imapserver
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"runtime/debug"
"strings"
"sync"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
const (
cmdReadTimeout = 30 * time.Second
idleReadTimeout = 35 * time.Minute // section 5.4 says 30min minimum
literalReadTimeout = 5 * time.Minute
respWriteTimeout = 30 * time.Second
literalWriteTimeout = 5 * time.Minute
maxCommandSize = 50 * 1024 // RFC 2683 section 3.2.1.5 says 8KiB minimum
)
var internalServerErrorResp = &imap.StatusResponse{
Type: imap.StatusResponseTypeNo,
Code: imap.ResponseCodeServerBug,
Text: "Internal server error",
}
// A Conn represents an IMAP connection to the server.
type Conn struct {
server *Server
br *bufio.Reader
bw *bufio.Writer
encMutex sync.Mutex
mutex sync.Mutex
conn net.Conn
enabled imap.CapSet
state imap.ConnState
session Session
}
func newConn(c net.Conn, server *Server) *Conn {
rw := server.options.wrapReadWriter(c)
br := bufio.NewReader(rw)
bw := bufio.NewWriter(rw)
return &Conn{
conn: c,
server: server,
br: br,
bw: bw,
enabled: make(imap.CapSet),
}
}
// NetConn returns the underlying connection that is wrapped by the IMAP
// connection.
//
// Writing to or reading from this connection directly will corrupt the IMAP
// session.
func (c *Conn) NetConn() net.Conn {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.conn
}
// Bye terminates the IMAP connection.
func (c *Conn) Bye(text string) error {
respErr := c.writeStatusResp("", &imap.StatusResponse{
Type: imap.StatusResponseTypeBye,
Text: text,
})
closeErr := c.conn.Close()
if respErr != nil {
return respErr
}
return closeErr
}
func (c *Conn) EnabledCaps() imap.CapSet {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.enabled.Copy()
}
func (c *Conn) serve() {
defer func() {
if v := recover(); v != nil {
c.server.logger().Printf("panic handling command: %v\n%s", v, debug.Stack())
}
c.conn.Close()
}()
c.server.mutex.Lock()
c.server.conns[c] = struct{}{}
c.server.mutex.Unlock()
defer func() {
c.server.mutex.Lock()
delete(c.server.conns, c)
c.server.mutex.Unlock()
}()
var (
greetingData *GreetingData
err error
)
c.session, greetingData, err = c.server.options.NewSession(c)
if err != nil {
var (
resp *imap.StatusResponse
imapErr *imap.Error
)
if errors.As(err, &imapErr) && imapErr.Type == imap.StatusResponseTypeBye {
resp = (*imap.StatusResponse)(imapErr)
} else {
c.server.logger().Printf("failed to create session: %v", err)
resp = internalServerErrorResp
}
if err := c.writeStatusResp("", resp); err != nil {
c.server.logger().Printf("failed to write greeting: %v", err)
}
return
}
defer func() {
if c.session != nil {
if err := c.session.Close(); err != nil {
c.server.logger().Printf("failed to close session: %v", err)
}
}
}()
caps := c.server.options.caps()
if _, ok := c.session.(SessionIMAP4rev2); !ok && caps.Has(imap.CapIMAP4rev2) {
panic("imapserver: server advertises IMAP4rev2 but session doesn't support it")
}
if _, ok := c.session.(SessionNamespace); !ok && caps.Has(imap.CapNamespace) {
panic("imapserver: server advertises NAMESPACE but session doesn't support it")
}
if _, ok := c.session.(SessionMove); !ok && caps.Has(imap.CapMove) {
panic("imapserver: server advertises MOVE but session doesn't support it")
}
if _, ok := c.session.(SessionUnauthenticate); !ok && caps.Has(imap.CapUnauthenticate) {
panic("imapserver: server advertises UNAUTHENTICATE but session doesn't support it")
}
c.state = imap.ConnStateNotAuthenticated
statusType := imap.StatusResponseTypeOK
if greetingData != nil && greetingData.PreAuth {
c.state = imap.ConnStateAuthenticated
statusType = imap.StatusResponseTypePreAuth
}
if err := c.writeCapabilityStatus("", statusType, "IMAP server ready"); err != nil {
c.server.logger().Printf("failed to write greeting: %v", err)
return
}
for {
var readTimeout time.Duration
switch c.state {
case imap.ConnStateAuthenticated, imap.ConnStateSelected:
readTimeout = idleReadTimeout
default:
readTimeout = cmdReadTimeout
}
c.setReadTimeout(readTimeout)
dec := imapwire.NewDecoder(c.br, imapwire.ConnSideServer)
dec.MaxSize = maxCommandSize
dec.CheckBufferedLiteralFunc = c.checkBufferedLiteral
if c.state == imap.ConnStateLogout || dec.EOF() {
break
}
c.setReadTimeout(cmdReadTimeout)
if err := c.readCommand(dec); err != nil {
if !errors.Is(err, net.ErrClosed) {
c.server.logger().Printf("failed to read command: %v", err)
}
break
}
}
}
func (c *Conn) readCommand(dec *imapwire.Decoder) error {
var tag, name string
if !dec.ExpectAtom(&tag) || !dec.ExpectSP() || !dec.ExpectAtom(&name) {
return fmt.Errorf("in command: %w", dec.Err())
}
name = strings.ToUpper(name)
numKind := NumKindSeq
if name == "UID" {
numKind = NumKindUID
var subName string
if !dec.ExpectSP() || !dec.ExpectAtom(&subName) {
return fmt.Errorf("in command: %w", dec.Err())
}
name = "UID " + strings.ToUpper(subName)
}
// TODO: handle multiple commands concurrently
sendOK := true
var err error
switch name {
case "NOOP", "CHECK":
err = c.handleNoop(dec)
case "LOGOUT":
err = c.handleLogout(dec)
case "CAPABILITY":
err = c.handleCapability(dec)
case "STARTTLS":
err = c.handleStartTLS(tag, dec)
sendOK = false
case "AUTHENTICATE":
err = c.handleAuthenticate(tag, dec)
sendOK = false
case "UNAUTHENTICATE":
err = c.handleUnauthenticate(dec)
case "LOGIN":
err = c.handleLogin(tag, dec)
sendOK = false
case "ENABLE":
err = c.handleEnable(dec)
case "CREATE":
err = c.handleCreate(dec)
case "DELETE":
err = c.handleDelete(dec)
case "RENAME":
err = c.handleRename(dec)
case "SUBSCRIBE":
err = c.handleSubscribe(dec)
case "UNSUBSCRIBE":
err = c.handleUnsubscribe(dec)
case "STATUS":
err = c.handleStatus(dec)
case "LIST":
err = c.handleList(dec)
case "LSUB":
err = c.handleLSub(dec)
case "NAMESPACE":
err = c.handleNamespace(dec)
case "IDLE":
err = c.handleIdle(dec)
case "SELECT", "EXAMINE":
err = c.handleSelect(tag, dec, name == "EXAMINE")
sendOK = false
case "CLOSE", "UNSELECT":
err = c.handleUnselect(dec, name == "CLOSE")
case "APPEND":
err = c.handleAppend(tag, dec)
sendOK = false
case "FETCH", "UID FETCH":
err = c.handleFetch(dec, numKind)
case "EXPUNGE":
err = c.handleExpunge(dec)
case "UID EXPUNGE":
err = c.handleUIDExpunge(dec)
case "STORE", "UID STORE":
err = c.handleStore(dec, numKind)
case "COPY", "UID COPY":
err = c.handleCopy(tag, dec, numKind)
sendOK = false
case "MOVE", "UID MOVE":
err = c.handleMove(dec, numKind)
case "SEARCH", "UID SEARCH":
err = c.handleSearch(tag, dec, numKind)
default:
if c.state == imap.ConnStateNotAuthenticated {
// Don't allow a single unknown command before authentication to
// mitigate cross-protocol attacks:
// https://www-archive.mozilla.org/projects/netlib/portbanning
c.state = imap.ConnStateLogout
defer c.Bye("Unknown command")
}
err = &imap.Error{
Type: imap.StatusResponseTypeBad,
Text: "Unknown command",
}
}
dec.DiscardLine()
var (
resp *imap.StatusResponse
imapErr *imap.Error
decErr *imapwire.DecoderExpectError
)
if errors.As(err, &imapErr) {
resp = (*imap.StatusResponse)(imapErr)
} else if errors.As(err, &decErr) {
resp = &imap.StatusResponse{
Type: imap.StatusResponseTypeBad,
Code: imap.ResponseCodeClientBug,
Text: "Syntax error: " + decErr.Message,
}
} else if err != nil {
c.server.logger().Printf("handling %v command: %v", name, err)
resp = internalServerErrorResp
} else {
if !sendOK {
return nil
}
if err := c.poll(name); err != nil {
return err
}
resp = &imap.StatusResponse{
Type: imap.StatusResponseTypeOK,
Text: fmt.Sprintf("%v completed", name),
}
}
return c.writeStatusResp(tag, resp)
}
func (c *Conn) handleNoop(dec *imapwire.Decoder) error {
if !dec.ExpectCRLF() {
return dec.Err()
}
return nil
}
func (c *Conn) handleLogout(dec *imapwire.Decoder) error {
if !dec.ExpectCRLF() {
return dec.Err()
}
c.state = imap.ConnStateLogout
return c.writeStatusResp("", &imap.StatusResponse{
Type: imap.StatusResponseTypeBye,
Text: "Logging out",
})
}
func (c *Conn) handleDelete(dec *imapwire.Decoder) error {
var name string
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) || !dec.ExpectCRLF() {
return dec.Err()
}
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
return err
}
return c.session.Delete(name)
}
func (c *Conn) handleRename(dec *imapwire.Decoder) error {
var oldName, newName string
if !dec.ExpectSP() || !dec.ExpectMailbox(&oldName) || !dec.ExpectSP() || !dec.ExpectMailbox(&newName) || !dec.ExpectCRLF() {
return dec.Err()
}
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
return err
}
return c.session.Rename(oldName, newName)
}
func (c *Conn) handleSubscribe(dec *imapwire.Decoder) error {
var name string
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) || !dec.ExpectCRLF() {
return dec.Err()
}
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
return err
}
return c.session.Subscribe(name)
}
func (c *Conn) handleUnsubscribe(dec *imapwire.Decoder) error {
var name string
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) || !dec.ExpectCRLF() {
return dec.Err()
}
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
return err
}
return c.session.Unsubscribe(name)
}
func (c *Conn) checkBufferedLiteral(size int64, nonSync bool) error {
if size > 4096 {
return &imap.Error{
Type: imap.StatusResponseTypeNo,
Code: imap.ResponseCodeTooBig,
Text: "Literals are limited to 4096 bytes for this command",
}
}
return c.acceptLiteral(size, nonSync)
}
func (c *Conn) acceptLiteral(size int64, nonSync bool) error {
if nonSync && size > 4096 && !c.server.options.caps().Has(imap.CapLiteralPlus) {
return &imap.Error{
Type: imap.StatusResponseTypeBad,
Text: "Non-synchronizing literals are limited to 4096 bytes",
}
}
if nonSync {
return nil
}
return c.writeContReq("Ready for literal data")
}
func (c *Conn) canAuth() bool {
if c.state != imap.ConnStateNotAuthenticated {
return false
}
_, isTLS := c.conn.(*tls.Conn)
return isTLS || c.server.options.InsecureAuth
}
func (c *Conn) writeStatusResp(tag string, statusResp *imap.StatusResponse) error {
enc := newResponseEncoder(c)
defer enc.end()
return writeStatusResp(enc.Encoder, tag, statusResp)
}
func (c *Conn) writeContReq(text string) error {
enc := newResponseEncoder(c)
defer enc.end()
return writeContReq(enc.Encoder, text)
}
func (c *Conn) writeCapabilityStatus(tag string, typ imap.StatusResponseType, text string) error {
enc := newResponseEncoder(c)
defer enc.end()
return writeCapabilityStatus(enc.Encoder, tag, typ, c.availableCaps(), text)
}
func (c *Conn) checkState(state imap.ConnState) error {
if state == imap.ConnStateAuthenticated && c.state == imap.ConnStateSelected {
return nil
}
if c.state != state {
return newClientBugError(fmt.Sprintf("This command is only valid in the %s state", state))
}
return nil
}
func (c *Conn) setReadTimeout(dur time.Duration) {
if dur > 0 {
c.conn.SetReadDeadline(time.Now().Add(dur))
} else {
c.conn.SetReadDeadline(time.Time{})
}
}
func (c *Conn) setWriteTimeout(dur time.Duration) {
if dur > 0 {
c.conn.SetWriteDeadline(time.Now().Add(dur))
} else {
c.conn.SetWriteDeadline(time.Time{})
}
}
func (c *Conn) poll(cmd string) error {
switch c.state {
case imap.ConnStateAuthenticated, imap.ConnStateSelected:
// nothing to do
default:
return nil
}
allowExpunge := true
switch cmd {
case "FETCH", "STORE", "SEARCH":
allowExpunge = false
}
w := &UpdateWriter{conn: c, allowExpunge: allowExpunge}
return c.session.Poll(w, allowExpunge)
}
type responseEncoder struct {
*imapwire.Encoder
conn *Conn
}
func newResponseEncoder(conn *Conn) *responseEncoder {
conn.mutex.Lock()
quotedUTF8 := conn.enabled.Has(imap.CapIMAP4rev2) || conn.enabled.Has(imap.CapUTF8Accept)
conn.mutex.Unlock()
wireEnc := imapwire.NewEncoder(conn.bw, imapwire.ConnSideServer)
wireEnc.QuotedUTF8 = quotedUTF8
conn.encMutex.Lock() // released by responseEncoder.end
conn.setWriteTimeout(respWriteTimeout)
return &responseEncoder{
Encoder: wireEnc,
conn: conn,
}
}
func (enc *responseEncoder) end() {
if enc.Encoder == nil {
panic("imapserver: responseEncoder.end called twice")
}
enc.Encoder = nil
enc.conn.setWriteTimeout(0)
enc.conn.encMutex.Unlock()
}
func (enc *responseEncoder) Literal(size int64) io.WriteCloser {
enc.conn.setWriteTimeout(literalWriteTimeout)
return literalWriter{
WriteCloser: enc.Encoder.Literal(size, nil),
conn: enc.conn,
}
}
type literalWriter struct {
io.WriteCloser
conn *Conn
}
func (lw literalWriter) Close() error {
lw.conn.setWriteTimeout(respWriteTimeout)
return lw.WriteCloser.Close()
}
func writeStatusResp(enc *imapwire.Encoder, tag string, statusResp *imap.StatusResponse) error {
if tag == "" {
tag = "*"
}
enc.Atom(tag).SP().Atom(string(statusResp.Type)).SP()
if statusResp.Code != "" {
enc.Atom(fmt.Sprintf("[%v]", statusResp.Code)).SP()
}
enc.Text(statusResp.Text)
return enc.CRLF()
}
func writeCapabilityOK(enc *imapwire.Encoder, tag string, caps []imap.Cap, text string) error {
return writeCapabilityStatus(enc, tag, imap.StatusResponseTypeOK, caps, text)
}
func writeCapabilityStatus(enc *imapwire.Encoder, tag string, typ imap.StatusResponseType, caps []imap.Cap, text string) error {
if tag == "" {
tag = "*"
}
enc.Atom(tag).SP().Atom(string(typ)).SP().Special('[').Atom("CAPABILITY")
for _, c := range caps {
enc.SP().Atom(string(c))
}
enc.Special(']').SP().Text(text)
return enc.CRLF()
}
func writeContReq(enc *imapwire.Encoder, text string) error {
return enc.Atom("+").SP().Text(text).CRLF()
}
func newClientBugError(text string) error {
return &imap.Error{
Type: imap.StatusResponseTypeBad,
Code: imap.ResponseCodeClientBug,
Text: text,
}
}
// UpdateWriter writes status updates.
type UpdateWriter struct {
conn *Conn
allowExpunge bool
}
// WriteExpunge writes an EXPUNGE response.
func (w *UpdateWriter) WriteExpunge(seqNum uint32) error {
if !w.allowExpunge {
return fmt.Errorf("imapserver: EXPUNGE updates are not allowed in this context")
}
return w.conn.writeExpunge(seqNum)
}
// WriteNumMessages writes an EXISTS response.
func (w *UpdateWriter) WriteNumMessages(n uint32) error {
return w.conn.writeExists(n)
}
// WriteNumRecent writes an RECENT response (not used in IMAP4rev2, will be ignored).
func (w *UpdateWriter) WriteNumRecent(n uint32) error {
if w.conn.enabled.Has(imap.CapIMAP4rev2) || !w.conn.server.options.caps().Has(imap.CapIMAP4rev1) {
return nil
}
return w.conn.writeObsoleteRecent(n)
}
// WriteMailboxFlags writes a FLAGS response.
func (w *UpdateWriter) WriteMailboxFlags(flags []imap.Flag) error {
return w.conn.writeFlags(flags)
}
// WriteMessageFlags writes a FETCH response with FLAGS.
func (w *UpdateWriter) WriteMessageFlags(seqNum uint32, uid imap.UID, flags []imap.Flag) error {
fetchWriter := &FetchWriter{conn: w.conn}
respWriter := fetchWriter.CreateMessage(seqNum)
if uid != 0 {
respWriter.WriteUID(uid)
}
respWriter.WriteFlags(flags)
return respWriter.Close()
}