Forked the emersion/go-imap v1 project.
This commit is contained in:
617
imapserver/conn.go
Normal file
617
imapserver/conn.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user