Added files.
This commit is contained in:
123
imapserver/append.go
Normal file
123
imapserver/append.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// defaultAppendLimit is the default maximum size of an APPEND payload.
|
||||
const defaultAppendLimit = 100 * 1024 * 1024 // 100MiB
|
||||
|
||||
func (c *Conn) handleAppend(tag string, dec *imapwire.Decoder) error {
|
||||
var (
|
||||
mailbox string
|
||||
options imap.AppendOptions
|
||||
)
|
||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&mailbox) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
hasFlagList, err := dec.List(func() error {
|
||||
flag, err := internal.ExpectFlag(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Flags = append(options.Flags, flag)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasFlagList && !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
t, err := internal.DecodeDateTime(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !t.IsZero() && !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
options.Time = t
|
||||
|
||||
var dataExt string
|
||||
if !dec.Special('~') && dec.Atom(&dataExt) { // ignore literal8 prefix if any for BINARY
|
||||
switch strings.ToUpper(dataExt) {
|
||||
case "UTF8":
|
||||
// '~' is the literal8 prefix
|
||||
if !dec.ExpectSP() || !dec.ExpectSpecial('(') || !dec.ExpectSpecial('~') {
|
||||
return dec.Err()
|
||||
}
|
||||
default:
|
||||
return newClientBugError("Unknown APPEND data extension")
|
||||
}
|
||||
}
|
||||
|
||||
lit, nonSync, err := dec.ExpectLiteralReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appendLimit := int64(defaultAppendLimit)
|
||||
if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {
|
||||
appendLimit = int64(appendLimitSession.AppendLimit())
|
||||
}
|
||||
|
||||
if lit.Size() > appendLimit {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTooBig,
|
||||
Text: fmt.Sprintf("Literals are limited to %v bytes for this command", appendLimit),
|
||||
}
|
||||
}
|
||||
if err := c.acceptLiteral(lit.Size(), nonSync); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.setReadTimeout(literalReadTimeout)
|
||||
defer c.setReadTimeout(cmdReadTimeout)
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
io.Copy(io.Discard, lit)
|
||||
dec.CRLF()
|
||||
return err
|
||||
}
|
||||
|
||||
data, appendErr := c.session.Append(mailbox, lit, &options)
|
||||
if _, discardErr := io.Copy(io.Discard, lit); discardErr != nil {
|
||||
return err
|
||||
}
|
||||
if dataExt != "" && !dec.ExpectSpecial(')') {
|
||||
return dec.Err()
|
||||
}
|
||||
if !dec.ExpectCRLF() {
|
||||
return err
|
||||
}
|
||||
if appendErr != nil {
|
||||
return appendErr
|
||||
}
|
||||
if err := c.poll("APPEND"); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.writeAppendOK(tag, data)
|
||||
}
|
||||
|
||||
func (c *Conn) writeAppendOK(tag string, data *imap.AppendData) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom(tag).SP().Atom("OK").SP()
|
||||
if data != nil {
|
||||
enc.Special('[')
|
||||
enc.Atom("APPENDUID").SP().Number(data.UIDValidity).SP().UID(data.UID)
|
||||
enc.Special(']').SP()
|
||||
}
|
||||
enc.Text("APPEND completed")
|
||||
return enc.CRLF()
|
||||
}
|
||||
148
imapserver/authenticate.go
Normal file
148
imapserver/authenticate.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleAuthenticate(tag string, dec *imapwire.Decoder) error {
|
||||
var mech string
|
||||
if !dec.ExpectSP() || !dec.ExpectAtom(&mech) {
|
||||
return dec.Err()
|
||||
}
|
||||
mech = strings.ToUpper(mech)
|
||||
|
||||
var initialResp []byte
|
||||
if dec.SP() {
|
||||
var initialRespStr string
|
||||
if !dec.ExpectText(&initialRespStr) {
|
||||
return dec.Err()
|
||||
}
|
||||
var err error
|
||||
initialResp, err = internal.DecodeSASL(initialRespStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateNotAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
if !c.canAuth() {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodePrivacyRequired,
|
||||
Text: "TLS is required to authenticate",
|
||||
}
|
||||
}
|
||||
|
||||
var saslServer sasl.Server
|
||||
if authSess, ok := c.session.(SessionSASL); ok {
|
||||
var err error
|
||||
saslServer, err = authSess.Authenticate(mech)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if mech != "PLAIN" {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Text: "SASL mechanism not supported",
|
||||
}
|
||||
}
|
||||
saslServer = sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
if identity != "" && identity != username {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeAuthorizationFailed,
|
||||
Text: "SASL identity not supported",
|
||||
}
|
||||
}
|
||||
return c.session.Login(username, password)
|
||||
})
|
||||
}
|
||||
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
resp := initialResp
|
||||
for {
|
||||
challenge, done, err := saslServer.Next(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if done {
|
||||
break
|
||||
}
|
||||
|
||||
var challengeStr string
|
||||
if challenge != nil {
|
||||
challengeStr = internal.EncodeSASL(challenge)
|
||||
}
|
||||
if err := writeContReq(enc.Encoder, challengeStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encodedResp, isPrefix, err := c.br.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if isPrefix {
|
||||
return fmt.Errorf("SASL response too long")
|
||||
} else if string(encodedResp) == "*" {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeBad,
|
||||
Text: "AUTHENTICATE cancelled",
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = decodeSASL(string(encodedResp))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.state = imap.ConnStateAuthenticated
|
||||
text := fmt.Sprintf("%v authentication successful", mech)
|
||||
return writeCapabilityOK(enc.Encoder, tag, c.availableCaps(), text)
|
||||
}
|
||||
|
||||
func decodeSASL(s string) ([]byte, error) {
|
||||
b, err := internal.DecodeSASL(s)
|
||||
if err != nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeBad,
|
||||
Text: "Malformed SASL response",
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *Conn) handleUnauthenticate(dec *imapwire.Decoder) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
session, ok := c.session.(SessionUnauthenticate)
|
||||
if !ok {
|
||||
return newClientBugError("UNAUTHENTICATE is not supported")
|
||||
}
|
||||
if err := session.Unauthenticate(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = imap.ConnStateNotAuthenticated
|
||||
c.mutex.Lock()
|
||||
c.enabled = make(imap.CapSet)
|
||||
c.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
114
imapserver/capability.go
Normal file
114
imapserver/capability.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleCapability(dec *imapwire.Decoder) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("CAPABILITY")
|
||||
for _, c := range c.availableCaps() {
|
||||
enc.SP().Atom(string(c))
|
||||
}
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
// availableCaps returns the capabilities supported by the server.
|
||||
//
|
||||
// They depend on the connection state.
|
||||
//
|
||||
// Some extensions (e.g. SASL-IR, ENABLE) don't require backend support and
|
||||
// thus are always enabled.
|
||||
func (c *Conn) availableCaps() []imap.Cap {
|
||||
available := c.server.options.caps()
|
||||
|
||||
var caps []imap.Cap
|
||||
addAvailableCaps(&caps, available, []imap.Cap{
|
||||
imap.CapIMAP4rev2,
|
||||
imap.CapIMAP4rev1,
|
||||
})
|
||||
if len(caps) == 0 {
|
||||
panic("imapserver: must support at least IMAP4rev1 or IMAP4rev2")
|
||||
}
|
||||
|
||||
if available.Has(imap.CapIMAP4rev1) {
|
||||
caps = append(caps, []imap.Cap{
|
||||
imap.CapSASLIR,
|
||||
imap.CapLiteralMinus,
|
||||
}...)
|
||||
}
|
||||
if c.canStartTLS() {
|
||||
caps = append(caps, imap.CapStartTLS)
|
||||
}
|
||||
if c.canAuth() {
|
||||
mechs := []string{"PLAIN"}
|
||||
if authSess, ok := c.session.(SessionSASL); ok {
|
||||
mechs = authSess.AuthenticateMechanisms()
|
||||
}
|
||||
for _, mech := range mechs {
|
||||
caps = append(caps, imap.Cap("AUTH="+mech))
|
||||
}
|
||||
} else if c.state == imap.ConnStateNotAuthenticated {
|
||||
caps = append(caps, imap.CapLoginDisabled)
|
||||
}
|
||||
if c.state == imap.ConnStateAuthenticated || c.state == imap.ConnStateSelected {
|
||||
if available.Has(imap.CapIMAP4rev1) {
|
||||
// IMAP4rev1-specific capabilities that don't require backend
|
||||
// support and are not applicable to IMAP4rev2
|
||||
caps = append(caps, []imap.Cap{
|
||||
imap.CapUnselect,
|
||||
imap.CapEnable,
|
||||
imap.CapIdle,
|
||||
imap.CapUTF8Accept,
|
||||
}...)
|
||||
|
||||
// IMAP4rev1-specific capabilities which require backend support
|
||||
// and are not applicable to IMAP4rev2
|
||||
addAvailableCaps(&caps, available, []imap.Cap{
|
||||
imap.CapNamespace,
|
||||
imap.CapUIDPlus,
|
||||
imap.CapESearch,
|
||||
imap.CapSearchRes,
|
||||
imap.CapListExtended,
|
||||
imap.CapListStatus,
|
||||
imap.CapMove,
|
||||
imap.CapStatusSize,
|
||||
imap.CapBinary,
|
||||
imap.CapChildren,
|
||||
})
|
||||
}
|
||||
|
||||
// Capabilities which require backend support and apply to both
|
||||
// IMAP4rev1 and IMAP4rev2
|
||||
addAvailableCaps(&caps, available, []imap.Cap{
|
||||
imap.CapSpecialUse,
|
||||
imap.CapCreateSpecialUse,
|
||||
imap.CapLiteralPlus,
|
||||
imap.CapUnauthenticate,
|
||||
})
|
||||
|
||||
if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {
|
||||
limit := appendLimitSession.AppendLimit()
|
||||
caps = append(caps, imap.Cap(fmt.Sprintf("APPENDLIMIT=%d", limit)))
|
||||
} else {
|
||||
addAvailableCaps(&caps, available, []imap.Cap{imap.CapAppendLimit})
|
||||
}
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
func addAvailableCaps(caps *[]imap.Cap, available imap.CapSet, l []imap.Cap) {
|
||||
for _, c := range l {
|
||||
if available.Has(c) {
|
||||
*caps = append(*caps, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
618
imapserver/conn.go
Normal file
618
imapserver/conn.go
Normal file
@@ -0,0 +1,618 @@
|
||||
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
|
||||
}
|
||||
var options imap.RenameOptions
|
||||
return c.session.Rename(oldName, newName, &options)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
55
imapserver/copy.go
Normal file
55
imapserver/copy.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleCopy(tag string, dec *imapwire.Decoder, numKind NumKind) error {
|
||||
numSet, dest, err := readCopy(numKind, dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := c.session.Copy(numSet, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmdName := "COPY"
|
||||
if numKind == NumKindUID {
|
||||
cmdName = "UID COPY"
|
||||
}
|
||||
if err := c.poll(cmdName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.writeCopyOK(tag, data)
|
||||
}
|
||||
|
||||
func (c *Conn) writeCopyOK(tag string, data *imap.CopyData) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
if tag == "" {
|
||||
tag = "*"
|
||||
}
|
||||
|
||||
enc.Atom(tag).SP().Atom("OK").SP()
|
||||
if data != nil {
|
||||
enc.Special('[')
|
||||
enc.Atom("COPYUID").SP().Number(data.UIDValidity).SP().NumSet(data.SourceUIDs).SP().NumSet(data.DestUIDs)
|
||||
enc.Special(']').SP()
|
||||
}
|
||||
enc.Text("COPY completed")
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func readCopy(numKind NumKind, dec *imapwire.Decoder) (numSet imap.NumSet, dest string, err error) {
|
||||
if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() || !dec.ExpectMailbox(&dest) || !dec.ExpectCRLF() {
|
||||
return nil, "", dec.Err()
|
||||
}
|
||||
return numSet, dest, nil
|
||||
}
|
||||
45
imapserver/create.go
Normal file
45
imapserver/create.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleCreate(dec *imapwire.Decoder) error {
|
||||
var (
|
||||
name string
|
||||
options imap.CreateOptions
|
||||
)
|
||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) {
|
||||
return dec.Err()
|
||||
}
|
||||
if dec.SP() {
|
||||
var name string
|
||||
if !dec.ExpectSpecial('(') || !dec.ExpectAtom(&name) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
switch strings.ToUpper(name) {
|
||||
case "USE":
|
||||
var err error
|
||||
options.SpecialUse, err = internal.ExpectMailboxAttrList(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return newClientBugError("unknown CREATE parameter")
|
||||
}
|
||||
if !dec.ExpectSpecial(')') {
|
||||
return dec.Err()
|
||||
}
|
||||
}
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.session.Create(name, &options)
|
||||
}
|
||||
47
imapserver/enable.go
Normal file
47
imapserver/enable.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleEnable(dec *imapwire.Decoder) error {
|
||||
var requested []imap.Cap
|
||||
for dec.SP() {
|
||||
cap, err := internal.ExpectCap(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requested = append(requested, cap)
|
||||
}
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var enabled []imap.Cap
|
||||
for _, req := range requested {
|
||||
switch req {
|
||||
case imap.CapIMAP4rev2, imap.CapUTF8Accept:
|
||||
enabled = append(enabled, req)
|
||||
}
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
for _, e := range enabled {
|
||||
c.enabled[e] = struct{}{}
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("ENABLED")
|
||||
for _, c := range enabled {
|
||||
enc.SP().Atom(string(c))
|
||||
}
|
||||
return enc.CRLF()
|
||||
}
|
||||
50
imapserver/expunge.go
Normal file
50
imapserver/expunge.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleExpunge(dec *imapwire.Decoder) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
return c.expunge(nil)
|
||||
}
|
||||
|
||||
func (c *Conn) handleUIDExpunge(dec *imapwire.Decoder) error {
|
||||
var uidSet imap.UIDSet
|
||||
if !dec.ExpectSP() || !dec.ExpectUIDSet(&uidSet) || !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
return c.expunge(&uidSet)
|
||||
}
|
||||
|
||||
func (c *Conn) expunge(uids *imap.UIDSet) error {
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
w := &ExpungeWriter{conn: c}
|
||||
return c.session.Expunge(w, uids)
|
||||
}
|
||||
|
||||
func (c *Conn) writeExpunge(seqNum uint32) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Number(seqNum).SP().Atom("EXPUNGE")
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
// ExpungeWriter writes EXPUNGE updates.
|
||||
type ExpungeWriter struct {
|
||||
conn *Conn
|
||||
}
|
||||
|
||||
// WriteExpunge notifies the client that the message with the provided sequence
|
||||
// number has been deleted.
|
||||
func (w *ExpungeWriter) WriteExpunge(seqNum uint32) error {
|
||||
if w.conn == nil {
|
||||
return nil
|
||||
}
|
||||
return w.conn.writeExpunge(seqNum)
|
||||
}
|
||||
715
imapserver/fetch.go
Normal file
715
imapserver/fetch.go
Normal file
@@ -0,0 +1,715 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
const envelopeDateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||
|
||||
type fetchWriterOptions struct {
|
||||
bodyStructure struct {
|
||||
extended bool // BODYSTRUCTURE
|
||||
nonExtended bool // BODY
|
||||
}
|
||||
obsolete map[*imap.FetchItemBodySection]string
|
||||
}
|
||||
|
||||
func (c *Conn) handleFetch(dec *imapwire.Decoder, numKind NumKind) error {
|
||||
var numSet imap.NumSet
|
||||
if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
var options imap.FetchOptions
|
||||
writerOptions := fetchWriterOptions{obsolete: make(map[*imap.FetchItemBodySection]string)}
|
||||
isList, err := dec.List(func() error {
|
||||
name, err := readFetchAttName(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "ALL", "FAST", "FULL":
|
||||
return newClientBugError("FETCH macros are not allowed in a list")
|
||||
}
|
||||
return handleFetchAtt(dec, name, &options, &writerOptions)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isList {
|
||||
name, err := readFetchAttName(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle macros
|
||||
switch name {
|
||||
case "ALL":
|
||||
options.Flags = true
|
||||
options.InternalDate = true
|
||||
options.RFC822Size = true
|
||||
options.Envelope = true
|
||||
case "FAST":
|
||||
options.Flags = true
|
||||
options.InternalDate = true
|
||||
options.RFC822Size = true
|
||||
case "FULL":
|
||||
options.Flags = true
|
||||
options.InternalDate = true
|
||||
options.RFC822Size = true
|
||||
options.Envelope = true
|
||||
handleFetchBodyStructure(&options, &writerOptions, false)
|
||||
default:
|
||||
if err := handleFetchAtt(dec, name, &options, &writerOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if numKind == NumKindUID {
|
||||
options.UID = true
|
||||
}
|
||||
|
||||
w := &FetchWriter{conn: c, options: writerOptions}
|
||||
if err := c.session.Fetch(w, numSet, &options); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFetchAtt(dec *imapwire.Decoder, attName string, options *imap.FetchOptions, writerOptions *fetchWriterOptions) error {
|
||||
switch attName {
|
||||
case "BODYSTRUCTURE":
|
||||
handleFetchBodyStructure(options, writerOptions, true)
|
||||
case "ENVELOPE":
|
||||
options.Envelope = true
|
||||
case "FLAGS":
|
||||
options.Flags = true
|
||||
case "INTERNALDATE":
|
||||
options.InternalDate = true
|
||||
case "RFC822.SIZE":
|
||||
options.RFC822Size = true
|
||||
case "UID":
|
||||
options.UID = true
|
||||
case "RFC822": // equivalent to BODY[]
|
||||
bs := &imap.FetchItemBodySection{}
|
||||
writerOptions.obsolete[bs] = attName
|
||||
options.BodySection = append(options.BodySection, bs)
|
||||
case "RFC822.PEEK": // obsolete, equivalent to BODY.PEEK[], used by Outlook
|
||||
bs := &imap.FetchItemBodySection{Peek: true}
|
||||
writerOptions.obsolete[bs] = attName
|
||||
options.BodySection = append(options.BodySection, bs)
|
||||
case "RFC822.HEADER": // equivalent to BODY.PEEK[HEADER]
|
||||
bs := &imap.FetchItemBodySection{
|
||||
Specifier: imap.PartSpecifierHeader,
|
||||
Peek: true,
|
||||
}
|
||||
writerOptions.obsolete[bs] = attName
|
||||
options.BodySection = append(options.BodySection, bs)
|
||||
case "RFC822.TEXT": // equivalent to BODY[TEXT]
|
||||
bs := &imap.FetchItemBodySection{
|
||||
Specifier: imap.PartSpecifierText,
|
||||
}
|
||||
writerOptions.obsolete[bs] = attName
|
||||
options.BodySection = append(options.BodySection, bs)
|
||||
case "BINARY", "BINARY.PEEK":
|
||||
part, err := readSectionBinary(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
partial, err := maybeReadPartial(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bs := &imap.FetchItemBinarySection{
|
||||
Part: part,
|
||||
Partial: partial,
|
||||
Peek: attName == "BINARY.PEEK",
|
||||
}
|
||||
options.BinarySection = append(options.BinarySection, bs)
|
||||
case "BINARY.SIZE":
|
||||
part, err := readSectionBinary(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bss := &imap.FetchItemBinarySectionSize{Part: part}
|
||||
options.BinarySectionSize = append(options.BinarySectionSize, bss)
|
||||
case "BODY":
|
||||
if !dec.Special('[') {
|
||||
handleFetchBodyStructure(options, writerOptions, false)
|
||||
return nil
|
||||
}
|
||||
section := imap.FetchItemBodySection{}
|
||||
err := readSection(dec, §ion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
section.Partial, err = maybeReadPartial(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.BodySection = append(options.BodySection, §ion)
|
||||
case "BODY.PEEK":
|
||||
if !dec.ExpectSpecial('[') {
|
||||
return dec.Err()
|
||||
}
|
||||
section := imap.FetchItemBodySection{Peek: true}
|
||||
err := readSection(dec, §ion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
section.Partial, err = maybeReadPartial(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.BodySection = append(options.BodySection, §ion)
|
||||
default:
|
||||
return newClientBugError("Unknown FETCH data item")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFetchBodyStructure(options *imap.FetchOptions, writerOptions *fetchWriterOptions, extended bool) {
|
||||
if options.BodyStructure == nil || extended {
|
||||
options.BodyStructure = &imap.FetchItemBodyStructure{Extended: extended}
|
||||
}
|
||||
if extended {
|
||||
writerOptions.bodyStructure.extended = true
|
||||
} else {
|
||||
writerOptions.bodyStructure.nonExtended = true
|
||||
}
|
||||
}
|
||||
|
||||
func readFetchAttName(dec *imapwire.Decoder) (string, error) {
|
||||
var attName string
|
||||
if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") {
|
||||
return "", dec.Err()
|
||||
}
|
||||
return strings.ToUpper(attName), nil
|
||||
}
|
||||
|
||||
func isMsgAttNameChar(ch byte) bool {
|
||||
return ch != '[' && imapwire.IsAtomChar(ch)
|
||||
}
|
||||
|
||||
func readSection(dec *imapwire.Decoder, section *imap.FetchItemBodySection) error {
|
||||
if dec.Special(']') {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dot bool
|
||||
section.Part, dot = readSectionPart(dec)
|
||||
if dot || len(section.Part) == 0 {
|
||||
var specifier string
|
||||
if dot {
|
||||
if !dec.ExpectAtom(&specifier) {
|
||||
return dec.Err()
|
||||
}
|
||||
} else {
|
||||
dec.Atom(&specifier)
|
||||
}
|
||||
|
||||
switch specifier := imap.PartSpecifier(strings.ToUpper(specifier)); specifier {
|
||||
case imap.PartSpecifierNone, imap.PartSpecifierHeader, imap.PartSpecifierMIME, imap.PartSpecifierText:
|
||||
section.Specifier = specifier
|
||||
case "HEADER.FIELDS", "HEADER.FIELDS.NOT":
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
var err error
|
||||
headerList, err := readHeaderList(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
section.Specifier = imap.PartSpecifierHeader
|
||||
if specifier == "HEADER.FIELDS" {
|
||||
section.HeaderFields = headerList
|
||||
} else {
|
||||
section.HeaderFieldsNot = headerList
|
||||
}
|
||||
default:
|
||||
return newClientBugError("unknown body section specifier")
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectSpecial(']') {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSectionPart(dec *imapwire.Decoder) (part []int, dot bool) {
|
||||
for {
|
||||
dot = len(part) > 0
|
||||
if dot && !dec.Special('.') {
|
||||
return part, false
|
||||
}
|
||||
|
||||
var num uint32
|
||||
if !dec.Number(&num) {
|
||||
return part, dot
|
||||
}
|
||||
part = append(part, int(num))
|
||||
}
|
||||
}
|
||||
|
||||
func readHeaderList(dec *imapwire.Decoder) ([]string, error) {
|
||||
var l []string
|
||||
err := dec.ExpectList(func() error {
|
||||
var s string
|
||||
if !dec.ExpectAString(&s) {
|
||||
return dec.Err()
|
||||
}
|
||||
l = append(l, s)
|
||||
return nil
|
||||
})
|
||||
return l, err
|
||||
}
|
||||
|
||||
func readSectionBinary(dec *imapwire.Decoder) ([]int, error) {
|
||||
if !dec.ExpectSpecial('[') {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
if dec.Special(']') {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var l []int
|
||||
for {
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return l, dec.Err()
|
||||
}
|
||||
l = append(l, int(num))
|
||||
|
||||
if !dec.Special('.') {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectSpecial(']') {
|
||||
return l, dec.Err()
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func maybeReadPartial(dec *imapwire.Decoder) (*imap.SectionPartial, error) {
|
||||
if !dec.Special('<') {
|
||||
return nil, nil
|
||||
}
|
||||
var partial imap.SectionPartial
|
||||
if !dec.ExpectNumber64(&partial.Offset) || !dec.ExpectSpecial('.') || !dec.ExpectNumber64(&partial.Size) || !dec.ExpectSpecial('>') {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
return &partial, nil
|
||||
}
|
||||
|
||||
// FetchWriter writes FETCH responses.
|
||||
type FetchWriter struct {
|
||||
conn *Conn
|
||||
options fetchWriterOptions
|
||||
}
|
||||
|
||||
// CreateMessage writes a FETCH response for a message.
|
||||
//
|
||||
// FetchResponseWriter.Close must be called.
|
||||
func (cmd *FetchWriter) CreateMessage(seqNum uint32) *FetchResponseWriter {
|
||||
enc := newResponseEncoder(cmd.conn)
|
||||
enc.Atom("*").SP().Number(seqNum).SP().Atom("FETCH").SP().Special('(')
|
||||
return &FetchResponseWriter{enc: enc, options: cmd.options}
|
||||
}
|
||||
|
||||
// FetchResponseWriter writes a single FETCH response for a message.
|
||||
type FetchResponseWriter struct {
|
||||
enc *responseEncoder
|
||||
options fetchWriterOptions
|
||||
|
||||
hasItem bool
|
||||
}
|
||||
|
||||
func (w *FetchResponseWriter) writeItemSep() {
|
||||
if w.hasItem {
|
||||
w.enc.SP()
|
||||
}
|
||||
w.hasItem = true
|
||||
}
|
||||
|
||||
// WriteUID writes the message's UID.
|
||||
func (w *FetchResponseWriter) WriteUID(uid imap.UID) {
|
||||
w.writeItemSep()
|
||||
w.enc.Atom("UID").SP().UID(uid)
|
||||
}
|
||||
|
||||
// WriteFlags writes the message's flags.
|
||||
func (w *FetchResponseWriter) WriteFlags(flags []imap.Flag) {
|
||||
w.writeItemSep()
|
||||
w.enc.Atom("FLAGS").SP().List(len(flags), func(i int) {
|
||||
w.enc.Flag(flags[i])
|
||||
})
|
||||
}
|
||||
|
||||
// WriteRFC822Size writes the message's full size.
|
||||
func (w *FetchResponseWriter) WriteRFC822Size(size int64) {
|
||||
w.writeItemSep()
|
||||
w.enc.Atom("RFC822.SIZE").SP().Number64(size)
|
||||
}
|
||||
|
||||
// WriteInternalDate writes the message's internal date.
|
||||
func (w *FetchResponseWriter) WriteInternalDate(t time.Time) {
|
||||
w.writeItemSep()
|
||||
w.enc.Atom("INTERNALDATE").SP().String(t.Format(internal.DateTimeLayout))
|
||||
}
|
||||
|
||||
// WriteBodySection writes a body section.
|
||||
//
|
||||
// The returned io.WriteCloser must be closed before writing any more message
|
||||
// data items.
|
||||
func (w *FetchResponseWriter) WriteBodySection(section *imap.FetchItemBodySection, size int64) io.WriteCloser {
|
||||
w.writeItemSep()
|
||||
enc := w.enc.Encoder
|
||||
|
||||
if obs, ok := w.options.obsolete[section]; ok {
|
||||
enc.Atom(obs)
|
||||
} else {
|
||||
writeItemBodySection(enc, section)
|
||||
}
|
||||
|
||||
enc.SP()
|
||||
return w.enc.Literal(size)
|
||||
}
|
||||
|
||||
func writeItemBodySection(enc *imapwire.Encoder, section *imap.FetchItemBodySection) {
|
||||
enc.Atom("BODY")
|
||||
enc.Special('[')
|
||||
writeSectionPart(enc, section.Part)
|
||||
if len(section.Part) > 0 && section.Specifier != imap.PartSpecifierNone {
|
||||
enc.Special('.')
|
||||
}
|
||||
if section.Specifier != imap.PartSpecifierNone {
|
||||
enc.Atom(string(section.Specifier))
|
||||
|
||||
var headerList []string
|
||||
if len(section.HeaderFields) > 0 {
|
||||
headerList = section.HeaderFields
|
||||
enc.Atom(".FIELDS")
|
||||
} else if len(section.HeaderFieldsNot) > 0 {
|
||||
headerList = section.HeaderFieldsNot
|
||||
enc.Atom(".FIELDS.NOT")
|
||||
}
|
||||
|
||||
if len(headerList) > 0 {
|
||||
enc.SP().List(len(headerList), func(i int) {
|
||||
enc.String(headerList[i])
|
||||
})
|
||||
}
|
||||
}
|
||||
enc.Special(']')
|
||||
if partial := section.Partial; partial != nil {
|
||||
enc.Special('<').Number(uint32(partial.Offset)).Special('>')
|
||||
}
|
||||
}
|
||||
|
||||
// WriteBinarySection writes a binary section.
|
||||
//
|
||||
// The returned io.WriteCloser must be closed before writing any more message
|
||||
// data items.
|
||||
func (w *FetchResponseWriter) WriteBinarySection(section *imap.FetchItemBinarySection, size int64) io.WriteCloser {
|
||||
w.writeItemSep()
|
||||
enc := w.enc.Encoder
|
||||
|
||||
enc.Atom("BINARY").Special('[')
|
||||
writeSectionPart(enc, section.Part)
|
||||
enc.Special(']').SP()
|
||||
enc.Special('~') // indicates literal8
|
||||
return w.enc.Literal(size)
|
||||
}
|
||||
|
||||
// WriteBinarySectionSize writes a binary section size.
|
||||
func (w *FetchResponseWriter) WriteBinarySectionSize(section *imap.FetchItemBinarySectionSize, size uint32) {
|
||||
w.writeItemSep()
|
||||
enc := w.enc.Encoder
|
||||
|
||||
enc.Atom("BINARY.SIZE").Special('[')
|
||||
writeSectionPart(enc, section.Part)
|
||||
enc.Special(']').SP().Number(size)
|
||||
}
|
||||
|
||||
// WriteEnvelope writes the message's envelope.
|
||||
func (w *FetchResponseWriter) WriteEnvelope(envelope *imap.Envelope) {
|
||||
w.writeItemSep()
|
||||
enc := w.enc.Encoder
|
||||
enc.Atom("ENVELOPE").SP()
|
||||
writeEnvelope(enc, envelope)
|
||||
}
|
||||
|
||||
// WriteBodyStructure writes the message's body structure (either BODYSTRUCTURE
|
||||
// or BODY).
|
||||
func (w *FetchResponseWriter) WriteBodyStructure(bs imap.BodyStructure) {
|
||||
if w.options.bodyStructure.nonExtended {
|
||||
w.writeBodyStructure(bs, false)
|
||||
}
|
||||
|
||||
if w.options.bodyStructure.extended {
|
||||
var isExtended bool
|
||||
switch bs := bs.(type) {
|
||||
case *imap.BodyStructureSinglePart:
|
||||
isExtended = bs.Extended != nil
|
||||
case *imap.BodyStructureMultiPart:
|
||||
isExtended = bs.Extended != nil
|
||||
}
|
||||
if !isExtended {
|
||||
panic("imapserver: client requested extended body structure but a non-extended one is written back")
|
||||
}
|
||||
|
||||
w.writeBodyStructure(bs, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *FetchResponseWriter) writeBodyStructure(bs imap.BodyStructure, extended bool) {
|
||||
item := "BODY"
|
||||
if extended {
|
||||
item = "BODYSTRUCTURE"
|
||||
}
|
||||
|
||||
w.writeItemSep()
|
||||
enc := w.enc.Encoder
|
||||
enc.Atom(item).SP()
|
||||
writeBodyStructure(enc, bs, extended)
|
||||
}
|
||||
|
||||
// Close closes the FETCH message writer.
|
||||
func (w *FetchResponseWriter) Close() error {
|
||||
if w.enc == nil {
|
||||
return fmt.Errorf("imapserver: FetchResponseWriter already closed")
|
||||
}
|
||||
err := w.enc.Special(')').CRLF()
|
||||
w.enc.end()
|
||||
w.enc = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func writeEnvelope(enc *imapwire.Encoder, envelope *imap.Envelope) {
|
||||
if envelope == nil {
|
||||
envelope = new(imap.Envelope)
|
||||
}
|
||||
|
||||
sender := envelope.Sender
|
||||
if sender == nil {
|
||||
sender = envelope.From
|
||||
}
|
||||
replyTo := envelope.ReplyTo
|
||||
if replyTo == nil {
|
||||
replyTo = envelope.From
|
||||
}
|
||||
|
||||
enc.Special('(')
|
||||
if envelope.Date.IsZero() {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.String(envelope.Date.Format(envelopeDateLayout))
|
||||
}
|
||||
enc.SP()
|
||||
writeNString(enc, mime.QEncoding.Encode("utf-8", envelope.Subject))
|
||||
addrs := [][]imap.Address{
|
||||
envelope.From,
|
||||
sender,
|
||||
replyTo,
|
||||
envelope.To,
|
||||
envelope.Cc,
|
||||
envelope.Bcc,
|
||||
}
|
||||
for _, l := range addrs {
|
||||
enc.SP()
|
||||
writeAddressList(enc, l)
|
||||
}
|
||||
enc.SP()
|
||||
if len(envelope.InReplyTo) > 0 {
|
||||
enc.String("<" + strings.Join(envelope.InReplyTo, "> <") + ">")
|
||||
} else {
|
||||
enc.NIL()
|
||||
}
|
||||
enc.SP()
|
||||
if envelope.MessageID != "" {
|
||||
enc.String("<" + envelope.MessageID + ">")
|
||||
} else {
|
||||
enc.NIL()
|
||||
}
|
||||
enc.Special(')')
|
||||
}
|
||||
|
||||
func writeAddressList(enc *imapwire.Encoder, l []imap.Address) {
|
||||
if len(l) == 0 {
|
||||
enc.NIL()
|
||||
return
|
||||
}
|
||||
|
||||
enc.List(len(l), func(i int) {
|
||||
addr := l[i]
|
||||
enc.Special('(')
|
||||
writeNString(enc, mime.QEncoding.Encode("utf-8", addr.Name))
|
||||
enc.SP().NIL().SP()
|
||||
writeNString(enc, addr.Mailbox)
|
||||
enc.SP()
|
||||
writeNString(enc, addr.Host)
|
||||
enc.Special(')')
|
||||
})
|
||||
}
|
||||
|
||||
func writeNString(enc *imapwire.Encoder, s string) {
|
||||
if s == "" {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.String(s)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSectionPart(enc *imapwire.Encoder, part []int) {
|
||||
if len(part) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var l []string
|
||||
for _, num := range part {
|
||||
l = append(l, fmt.Sprintf("%v", num))
|
||||
}
|
||||
enc.Atom(strings.Join(l, "."))
|
||||
}
|
||||
|
||||
func writeBodyStructure(enc *imapwire.Encoder, bs imap.BodyStructure, extended bool) {
|
||||
enc.Special('(')
|
||||
switch bs := bs.(type) {
|
||||
case *imap.BodyStructureSinglePart:
|
||||
writeBodyType1part(enc, bs, extended)
|
||||
case *imap.BodyStructureMultiPart:
|
||||
writeBodyTypeMpart(enc, bs, extended)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown body structure type %T", bs))
|
||||
}
|
||||
enc.Special(')')
|
||||
}
|
||||
|
||||
func writeBodyType1part(enc *imapwire.Encoder, bs *imap.BodyStructureSinglePart, extended bool) {
|
||||
enc.String(bs.Type).SP().String(bs.Subtype).SP()
|
||||
writeBodyFldParam(enc, bs.Params)
|
||||
enc.SP()
|
||||
writeNString(enc, bs.ID)
|
||||
enc.SP()
|
||||
writeNString(enc, bs.Description)
|
||||
enc.SP()
|
||||
if bs.Encoding == "" {
|
||||
enc.String("7bit")
|
||||
} else {
|
||||
// Outlook for iOS chokes on upper-case encodings
|
||||
enc.String(strings.ToLower(bs.Encoding))
|
||||
}
|
||||
enc.SP().Number(bs.Size)
|
||||
|
||||
if msg := bs.MessageRFC822; msg != nil {
|
||||
enc.SP()
|
||||
writeEnvelope(enc, msg.Envelope)
|
||||
enc.SP()
|
||||
writeBodyStructure(enc, msg.BodyStructure, extended)
|
||||
enc.SP().Number64(msg.NumLines)
|
||||
} else if text := bs.Text; text != nil {
|
||||
enc.SP().Number64(text.NumLines)
|
||||
}
|
||||
|
||||
if !extended {
|
||||
return
|
||||
}
|
||||
ext := bs.Extended
|
||||
|
||||
enc.SP()
|
||||
enc.NIL() // MD5
|
||||
enc.SP()
|
||||
writeBodyFldDsp(enc, ext.Disposition)
|
||||
enc.SP()
|
||||
writeBodyFldLang(enc, ext.Language)
|
||||
enc.SP()
|
||||
writeNString(enc, ext.Location)
|
||||
}
|
||||
|
||||
func writeBodyTypeMpart(enc *imapwire.Encoder, bs *imap.BodyStructureMultiPart, extended bool) {
|
||||
if len(bs.Children) == 0 {
|
||||
panic("imapserver: imap.BodyStructureMultiPart must have at least one child")
|
||||
}
|
||||
for _, child := range bs.Children {
|
||||
// ABNF for body-type-mpart doesn't have SP between body entries, and
|
||||
// Outlook for iOS chokes on SP
|
||||
writeBodyStructure(enc, child, extended)
|
||||
}
|
||||
|
||||
enc.SP().String(bs.Subtype)
|
||||
|
||||
if !extended {
|
||||
return
|
||||
}
|
||||
ext := bs.Extended
|
||||
|
||||
enc.SP()
|
||||
writeBodyFldParam(enc, ext.Params)
|
||||
enc.SP()
|
||||
writeBodyFldDsp(enc, ext.Disposition)
|
||||
enc.SP()
|
||||
writeBodyFldLang(enc, ext.Language)
|
||||
enc.SP()
|
||||
writeNString(enc, ext.Location)
|
||||
}
|
||||
|
||||
func writeBodyFldParam(enc *imapwire.Encoder, params map[string]string) {
|
||||
if len(params) == 0 {
|
||||
enc.NIL()
|
||||
return
|
||||
}
|
||||
|
||||
var l []string
|
||||
for k := range params {
|
||||
l = append(l, k)
|
||||
}
|
||||
sort.Strings(l)
|
||||
|
||||
enc.List(len(l), func(i int) {
|
||||
k := l[i]
|
||||
v := params[k]
|
||||
enc.String(k).SP().String(v)
|
||||
})
|
||||
}
|
||||
|
||||
func writeBodyFldDsp(enc *imapwire.Encoder, disp *imap.BodyStructureDisposition) {
|
||||
if disp == nil {
|
||||
enc.NIL()
|
||||
return
|
||||
}
|
||||
|
||||
enc.Special('(').String(disp.Value).SP()
|
||||
writeBodyFldParam(enc, disp.Params)
|
||||
enc.Special(')')
|
||||
}
|
||||
|
||||
func writeBodyFldLang(enc *imapwire.Encoder, l []string) {
|
||||
if len(l) == 0 {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.List(len(l), func(i int) {
|
||||
enc.String(l[i])
|
||||
})
|
||||
}
|
||||
}
|
||||
50
imapserver/idle.go
Normal file
50
imapserver/idle.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleIdle(dec *imapwire.Decoder) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.writeContReq("idling"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
c.server.logger().Printf("panic idling: %v\n%s", v, debug.Stack())
|
||||
done <- fmt.Errorf("imapserver: panic idling")
|
||||
}
|
||||
}()
|
||||
w := &UpdateWriter{conn: c, allowExpunge: true}
|
||||
done <- c.session.Idle(w, stop)
|
||||
}()
|
||||
|
||||
c.setReadTimeout(idleReadTimeout)
|
||||
line, isPrefix, err := c.br.ReadLine()
|
||||
close(stop)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if isPrefix || string(line) != "DONE" {
|
||||
return newClientBugError("Syntax error: expected DONE to end IDLE command")
|
||||
}
|
||||
|
||||
return <-done
|
||||
}
|
||||
511
imapserver/imapmemserver/mailbox.go
Normal file
511
imapserver/imapmemserver/mailbox.go
Normal file
@@ -0,0 +1,511 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
// Mailbox is an in-memory mailbox.
|
||||
//
|
||||
// The same mailbox can be shared between multiple connections and multiple
|
||||
// users.
|
||||
type Mailbox struct {
|
||||
tracker *imapserver.MailboxTracker
|
||||
uidValidity uint32
|
||||
|
||||
mutex sync.Mutex
|
||||
name string
|
||||
subscribed bool
|
||||
specialUse []imap.MailboxAttr
|
||||
l []*message
|
||||
uidNext imap.UID
|
||||
}
|
||||
|
||||
// NewMailbox creates a new mailbox.
|
||||
func NewMailbox(name string, uidValidity uint32) *Mailbox {
|
||||
return &Mailbox{
|
||||
tracker: imapserver.NewMailboxTracker(0),
|
||||
uidValidity: uidValidity,
|
||||
name: name,
|
||||
uidNext: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) list(options *imap.ListOptions) *imap.ListData {
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
|
||||
if options.SelectSubscribed && !mbox.subscribed {
|
||||
return nil
|
||||
}
|
||||
if options.SelectSpecialUse && len(mbox.specialUse) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := imap.ListData{
|
||||
Mailbox: mbox.name,
|
||||
Delim: mailboxDelim,
|
||||
}
|
||||
if mbox.subscribed {
|
||||
data.Attrs = append(data.Attrs, imap.MailboxAttrSubscribed)
|
||||
}
|
||||
if (options.ReturnSpecialUse || options.SelectSpecialUse) && len(mbox.specialUse) > 0 {
|
||||
data.Attrs = append(data.Attrs, mbox.specialUse...)
|
||||
}
|
||||
if options.ReturnStatus != nil {
|
||||
data.Status = mbox.statusDataLocked(options.ReturnStatus)
|
||||
}
|
||||
return &data
|
||||
}
|
||||
|
||||
// StatusData returns data for the STATUS command.
|
||||
func (mbox *Mailbox) StatusData(options *imap.StatusOptions) *imap.StatusData {
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
return mbox.statusDataLocked(options)
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusData {
|
||||
data := imap.StatusData{Mailbox: mbox.name}
|
||||
if options.NumMessages {
|
||||
num := uint32(len(mbox.l))
|
||||
data.NumMessages = &num
|
||||
}
|
||||
if options.UIDNext {
|
||||
data.UIDNext = mbox.uidNext
|
||||
}
|
||||
if options.UIDValidity {
|
||||
data.UIDValidity = mbox.uidValidity
|
||||
}
|
||||
if options.NumUnseen {
|
||||
num := uint32(len(mbox.l)) - mbox.countByFlagLocked(imap.FlagSeen)
|
||||
data.NumUnseen = &num
|
||||
}
|
||||
if options.NumDeleted {
|
||||
num := mbox.countByFlagLocked(imap.FlagDeleted)
|
||||
data.NumDeleted = &num
|
||||
}
|
||||
if options.Size {
|
||||
size := mbox.sizeLocked()
|
||||
data.Size = &size
|
||||
}
|
||||
if options.NumRecent {
|
||||
num := uint32(0)
|
||||
data.NumRecent = &num
|
||||
}
|
||||
return &data
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) countByFlagLocked(flag imap.Flag) uint32 {
|
||||
var n uint32
|
||||
for _, msg := range mbox.l {
|
||||
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) sizeLocked() int64 {
|
||||
var size int64
|
||||
for _, msg := range mbox.l {
|
||||
size += int64(len(msg.buf))
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) appendLiteral(r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.ReadFrom(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mbox.appendBytes(buf.Bytes(), options), nil
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) copyMsg(msg *message) *imap.AppendData {
|
||||
return mbox.appendBytes(msg.buf, &imap.AppendOptions{
|
||||
Time: msg.t,
|
||||
Flags: msg.flagList(),
|
||||
})
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) appendBytes(buf []byte, options *imap.AppendOptions) *imap.AppendData {
|
||||
msg := &message{
|
||||
flags: make(map[imap.Flag]struct{}),
|
||||
buf: buf,
|
||||
}
|
||||
|
||||
if options.Time.IsZero() {
|
||||
msg.t = time.Now()
|
||||
} else {
|
||||
msg.t = options.Time
|
||||
}
|
||||
|
||||
for _, flag := range options.Flags {
|
||||
msg.flags[canonicalFlag(flag)] = struct{}{}
|
||||
}
|
||||
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
|
||||
msg.uid = mbox.uidNext
|
||||
mbox.uidNext++
|
||||
|
||||
mbox.l = append(mbox.l, msg)
|
||||
mbox.tracker.QueueNumMessages(uint32(len(mbox.l)))
|
||||
|
||||
return &imap.AppendData{
|
||||
UIDValidity: mbox.uidValidity,
|
||||
UID: msg.uid,
|
||||
}
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) rename(newName string) {
|
||||
mbox.mutex.Lock()
|
||||
mbox.name = newName
|
||||
mbox.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetSubscribed changes the subscription state of this mailbox.
|
||||
func (mbox *Mailbox) SetSubscribed(subscribed bool) {
|
||||
mbox.mutex.Lock()
|
||||
mbox.subscribed = subscribed
|
||||
mbox.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) selectDataLocked() *imap.SelectData {
|
||||
flags := mbox.flagsLocked()
|
||||
|
||||
permanentFlags := make([]imap.Flag, len(flags))
|
||||
copy(permanentFlags, flags)
|
||||
permanentFlags = append(permanentFlags, imap.FlagWildcard)
|
||||
|
||||
// TODO: skip if IMAP4rev1 is disabled by the server, or IMAP4rev2 is
|
||||
// enabled by the client
|
||||
firstUnseenSeqNum := mbox.firstUnseenSeqNumLocked()
|
||||
|
||||
return &imap.SelectData{
|
||||
Flags: flags,
|
||||
PermanentFlags: permanentFlags,
|
||||
NumMessages: uint32(len(mbox.l)),
|
||||
FirstUnseenSeqNum: firstUnseenSeqNum,
|
||||
UIDNext: mbox.uidNext,
|
||||
UIDValidity: mbox.uidValidity,
|
||||
}
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) firstUnseenSeqNumLocked() uint32 {
|
||||
for i, msg := range mbox.l {
|
||||
seqNum := uint32(i) + 1
|
||||
if _, ok := msg.flags[canonicalFlag(imap.FlagSeen)]; !ok {
|
||||
return seqNum
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) flagsLocked() []imap.Flag {
|
||||
m := make(map[imap.Flag]struct{})
|
||||
for _, msg := range mbox.l {
|
||||
for flag := range msg.flags {
|
||||
m[flag] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var l []imap.Flag
|
||||
for flag := range m {
|
||||
l = append(l, flag)
|
||||
}
|
||||
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
return l[i] < l[j]
|
||||
})
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
|
||||
expunged := make(map[*message]struct{})
|
||||
mbox.mutex.Lock()
|
||||
for _, msg := range mbox.l {
|
||||
if uids != nil && !uids.Contains(msg.uid) {
|
||||
continue
|
||||
}
|
||||
if _, ok := msg.flags[canonicalFlag(imap.FlagDeleted)]; ok {
|
||||
expunged[msg] = struct{}{}
|
||||
}
|
||||
}
|
||||
mbox.mutex.Unlock()
|
||||
|
||||
if len(expunged) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mbox.mutex.Lock()
|
||||
mbox.expungeLocked(expunged)
|
||||
mbox.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mbox *Mailbox) expungeLocked(expunged map[*message]struct{}) (seqNums []uint32) {
|
||||
// TODO: optimize
|
||||
|
||||
// Iterate in reverse order, to keep sequence numbers consistent
|
||||
var filtered []*message
|
||||
for i := len(mbox.l) - 1; i >= 0; i-- {
|
||||
msg := mbox.l[i]
|
||||
if _, ok := expunged[msg]; ok {
|
||||
seqNum := uint32(i) + 1
|
||||
seqNums = append(seqNums, seqNum)
|
||||
mbox.tracker.QueueExpunge(seqNum)
|
||||
} else {
|
||||
filtered = append(filtered, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse filtered
|
||||
for i := 0; i < len(filtered)/2; i++ {
|
||||
j := len(filtered) - i - 1
|
||||
filtered[i], filtered[j] = filtered[j], filtered[i]
|
||||
}
|
||||
|
||||
mbox.l = filtered
|
||||
|
||||
return seqNums
|
||||
}
|
||||
|
||||
// NewView creates a new view into this mailbox.
|
||||
//
|
||||
// Callers must call MailboxView.Close once they are done with the mailbox view.
|
||||
func (mbox *Mailbox) NewView() *MailboxView {
|
||||
return &MailboxView{
|
||||
Mailbox: mbox,
|
||||
tracker: mbox.tracker.NewSession(),
|
||||
}
|
||||
}
|
||||
|
||||
// A MailboxView is a view into a mailbox.
|
||||
//
|
||||
// Each view has its own queue of pending unilateral updates.
|
||||
//
|
||||
// Once the mailbox view is no longer used, Close must be called.
|
||||
//
|
||||
// Typically, a new MailboxView is created for each IMAP connection in the
|
||||
// selected state.
|
||||
type MailboxView struct {
|
||||
*Mailbox
|
||||
tracker *imapserver.SessionTracker
|
||||
searchRes imap.UIDSet
|
||||
}
|
||||
|
||||
// Close releases the resources allocated for the mailbox view.
|
||||
func (mbox *MailboxView) Close() {
|
||||
mbox.tracker.Close()
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error {
|
||||
markSeen := false
|
||||
for _, bs := range options.BodySection {
|
||||
if !bs.Peek {
|
||||
markSeen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if markSeen {
|
||||
msg.flags[canonicalFlag(imap.FlagSeen)] = struct{}{}
|
||||
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), nil)
|
||||
}
|
||||
|
||||
respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum))
|
||||
err = msg.fetch(respWriter, options)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
|
||||
mbox.staticSearchCriteria(criteria)
|
||||
|
||||
var (
|
||||
data imap.SearchData
|
||||
seqSet imap.SeqSet
|
||||
uidSet imap.UIDSet
|
||||
)
|
||||
for i, msg := range mbox.l {
|
||||
seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1)
|
||||
|
||||
if !msg.search(seqNum, criteria) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Always populate the UID set, since it may be saved later for SEARCHRES
|
||||
uidSet.AddNum(msg.uid)
|
||||
|
||||
var num uint32
|
||||
switch numKind {
|
||||
case imapserver.NumKindSeq:
|
||||
if seqNum == 0 {
|
||||
continue
|
||||
}
|
||||
seqSet.AddNum(seqNum)
|
||||
num = seqNum
|
||||
case imapserver.NumKindUID:
|
||||
num = uint32(msg.uid)
|
||||
}
|
||||
if data.Min == 0 || num < data.Min {
|
||||
data.Min = num
|
||||
}
|
||||
if data.Max == 0 || num > data.Max {
|
||||
data.Max = num
|
||||
}
|
||||
data.Count++
|
||||
}
|
||||
|
||||
switch numKind {
|
||||
case imapserver.NumKindSeq:
|
||||
data.All = seqSet
|
||||
case imapserver.NumKindUID:
|
||||
data.All = uidSet
|
||||
}
|
||||
|
||||
if options.ReturnSave {
|
||||
mbox.searchRes = uidSet
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) staticSearchCriteria(criteria *imap.SearchCriteria) {
|
||||
seqNums := make([]imap.SeqSet, 0, len(criteria.SeqNum))
|
||||
for _, seqSet := range criteria.SeqNum {
|
||||
numSet := mbox.staticNumSet(seqSet)
|
||||
switch numSet := numSet.(type) {
|
||||
case imap.SeqSet:
|
||||
seqNums = append(seqNums, numSet)
|
||||
case imap.UIDSet: // can happen with SEARCHRES
|
||||
criteria.UID = append(criteria.UID, numSet)
|
||||
}
|
||||
}
|
||||
criteria.SeqNum = seqNums
|
||||
|
||||
for i, uidSet := range criteria.UID {
|
||||
criteria.UID[i] = mbox.staticNumSet(uidSet).(imap.UIDSet)
|
||||
}
|
||||
|
||||
for i := range criteria.Not {
|
||||
mbox.staticSearchCriteria(&criteria.Not[i])
|
||||
}
|
||||
for i := range criteria.Or {
|
||||
for j := range criteria.Or[i] {
|
||||
mbox.staticSearchCriteria(&criteria.Or[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
|
||||
mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||
msg.store(flags)
|
||||
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker)
|
||||
})
|
||||
if !flags.Silent {
|
||||
return mbox.Fetch(w, numSet, &imap.FetchOptions{Flags: true})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
||||
return mbox.tracker.Poll(w, allowExpunge)
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
||||
return mbox.tracker.Idle(w, stop)
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *message)) {
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
mbox.forEachLocked(numSet, f)
|
||||
}
|
||||
|
||||
func (mbox *MailboxView) forEachLocked(numSet imap.NumSet, f func(seqNum uint32, msg *message)) {
|
||||
// TODO: optimize
|
||||
|
||||
numSet = mbox.staticNumSet(numSet)
|
||||
|
||||
for i, msg := range mbox.l {
|
||||
seqNum := uint32(i) + 1
|
||||
|
||||
var contains bool
|
||||
switch numSet := numSet.(type) {
|
||||
case imap.SeqSet:
|
||||
seqNum := mbox.tracker.EncodeSeqNum(seqNum)
|
||||
contains = seqNum != 0 && numSet.Contains(seqNum)
|
||||
case imap.UIDSet:
|
||||
contains = numSet.Contains(msg.uid)
|
||||
}
|
||||
if !contains {
|
||||
continue
|
||||
}
|
||||
|
||||
f(seqNum, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// staticNumSet converts a dynamic sequence set into a static one.
|
||||
//
|
||||
// This is necessary to properly handle the special symbol "*", which
|
||||
// represents the maximum sequence number or UID in the mailbox.
|
||||
//
|
||||
// This function also handles the special SEARCHRES marker "$".
|
||||
func (mbox *MailboxView) staticNumSet(numSet imap.NumSet) imap.NumSet {
|
||||
if imap.IsSearchRes(numSet) {
|
||||
return mbox.searchRes
|
||||
}
|
||||
|
||||
switch numSet := numSet.(type) {
|
||||
case imap.SeqSet:
|
||||
max := uint32(len(mbox.l))
|
||||
for i := range numSet {
|
||||
r := &numSet[i]
|
||||
staticNumRange(&r.Start, &r.Stop, max)
|
||||
}
|
||||
case imap.UIDSet:
|
||||
max := uint32(mbox.uidNext) - 1
|
||||
for i := range numSet {
|
||||
r := &numSet[i]
|
||||
staticNumRange((*uint32)(&r.Start), (*uint32)(&r.Stop), max)
|
||||
}
|
||||
}
|
||||
|
||||
return numSet
|
||||
}
|
||||
|
||||
func staticNumRange(start, stop *uint32, max uint32) {
|
||||
dyn := false
|
||||
if *start == 0 {
|
||||
*start = max
|
||||
dyn = true
|
||||
}
|
||||
if *stop == 0 {
|
||||
*stop = max
|
||||
dyn = true
|
||||
}
|
||||
if dyn && *start > *stop {
|
||||
*start, *stop = *stop, *start
|
||||
}
|
||||
}
|
||||
273
imapserver/imapmemserver/message.go
Normal file
273
imapserver/imapmemserver/message.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
gomessage "github.com/emersion/go-message"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
// immutable
|
||||
uid imap.UID
|
||||
buf []byte
|
||||
t time.Time
|
||||
|
||||
// mutable, protected by Mailbox.mutex
|
||||
flags map[imap.Flag]struct{}
|
||||
}
|
||||
|
||||
func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error {
|
||||
w.WriteUID(msg.uid)
|
||||
|
||||
if options.Flags {
|
||||
w.WriteFlags(msg.flagList())
|
||||
}
|
||||
if options.InternalDate {
|
||||
w.WriteInternalDate(msg.t)
|
||||
}
|
||||
if options.RFC822Size {
|
||||
w.WriteRFC822Size(int64(len(msg.buf)))
|
||||
}
|
||||
if options.Envelope {
|
||||
w.WriteEnvelope(msg.envelope())
|
||||
}
|
||||
if options.BodyStructure != nil {
|
||||
w.WriteBodyStructure(imapserver.ExtractBodyStructure(bytes.NewReader(msg.buf)))
|
||||
}
|
||||
|
||||
for _, bs := range options.BodySection {
|
||||
buf := imapserver.ExtractBodySection(bytes.NewReader(msg.buf), bs)
|
||||
wc := w.WriteBodySection(bs, int64(len(buf)))
|
||||
_, writeErr := wc.Write(buf)
|
||||
closeErr := wc.Close()
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, bs := range options.BinarySection {
|
||||
buf := imapserver.ExtractBinarySection(bytes.NewReader(msg.buf), bs)
|
||||
wc := w.WriteBinarySection(bs, int64(len(buf)))
|
||||
_, writeErr := wc.Write(buf)
|
||||
closeErr := wc.Close()
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, bss := range options.BinarySectionSize {
|
||||
n := imapserver.ExtractBinarySectionSize(bytes.NewReader(msg.buf), bss)
|
||||
w.WriteBinarySectionSize(bss, n)
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (msg *message) envelope() *imap.Envelope {
|
||||
br := bufio.NewReader(bytes.NewReader(msg.buf))
|
||||
header, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return imapserver.ExtractEnvelope(header)
|
||||
}
|
||||
|
||||
func (msg *message) flagList() []imap.Flag {
|
||||
var flags []imap.Flag
|
||||
for flag := range msg.flags {
|
||||
flags = append(flags, flag)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (msg *message) store(store *imap.StoreFlags) {
|
||||
switch store.Op {
|
||||
case imap.StoreFlagsSet:
|
||||
msg.flags = make(map[imap.Flag]struct{})
|
||||
fallthrough
|
||||
case imap.StoreFlagsAdd:
|
||||
for _, flag := range store.Flags {
|
||||
msg.flags[canonicalFlag(flag)] = struct{}{}
|
||||
}
|
||||
case imap.StoreFlagsDel:
|
||||
for _, flag := range store.Flags {
|
||||
delete(msg.flags, canonicalFlag(flag))
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unknown STORE flag operation: %v", store.Op))
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *message) reader() *gomessage.Entity {
|
||||
r, _ := gomessage.Read(bytes.NewReader(msg.buf))
|
||||
if r == nil {
|
||||
r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(nil))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool {
|
||||
for _, seqSet := range criteria.SeqNum {
|
||||
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, uidSet := range criteria.UID {
|
||||
if !uidSet.Contains(msg.uid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, flag := range criteria.Flag {
|
||||
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, flag := range criteria.NotFlag {
|
||||
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if criteria.Larger != 0 && int64(len(msg.buf)) <= criteria.Larger {
|
||||
return false
|
||||
}
|
||||
if criteria.Smaller != 0 && int64(len(msg.buf)) >= criteria.Smaller {
|
||||
return false
|
||||
}
|
||||
|
||||
header := mail.Header{msg.reader().Header}
|
||||
|
||||
for _, fieldCriteria := range criteria.Header {
|
||||
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
||||
t, err := header.Date()
|
||||
if err != nil {
|
||||
return false
|
||||
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, text := range criteria.Text {
|
||||
if !matchEntity(msg.reader(), text, true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, body := range criteria.Body {
|
||||
if !matchEntity(msg.reader(), body, false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, not := range criteria.Not {
|
||||
if msg.search(seqNum, ¬) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range criteria.Or {
|
||||
if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func matchDate(t, since, before time.Time) bool {
|
||||
// We discard time zone information by setting it to UTC.
|
||||
// RFC 3501 explicitly requires zone unaware date comparison.
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if !since.IsZero() && t.Before(since) {
|
||||
return false
|
||||
}
|
||||
if !before.IsZero() && !t.Before(before) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchHeaderFields(fields gomessage.HeaderFields, pattern string) bool {
|
||||
if pattern == "" {
|
||||
return fields.Len() > 0
|
||||
}
|
||||
|
||||
pattern = strings.ToLower(pattern)
|
||||
for fields.Next() {
|
||||
v, _ := fields.Text()
|
||||
if strings.Contains(strings.ToLower(v), pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchEntity(e *gomessage.Entity, pattern string, includeHeader bool) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if includeHeader && matchHeaderFields(e.Header.Fields(), pattern) {
|
||||
return true
|
||||
}
|
||||
|
||||
if mr := e.MultipartReader(); mr != nil {
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if matchEntity(part, pattern, includeHeader) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
t, _, err := e.Header.ContentType()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(t, "text/") && !strings.HasPrefix(t, "message/") {
|
||||
return false
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(e.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Contains(bytes.ToLower(buf), bytes.ToLower([]byte(pattern)))
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalFlag(flag imap.Flag) imap.Flag {
|
||||
return imap.Flag(strings.ToLower(string(flag)))
|
||||
}
|
||||
61
imapserver/imapmemserver/server.go
Normal file
61
imapserver/imapmemserver/server.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Package imapmemserver implements an in-memory IMAP server.
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
// Server is a server instance.
|
||||
//
|
||||
// A server contains a list of users.
|
||||
type Server struct {
|
||||
mutex sync.Mutex
|
||||
users map[string]*User
|
||||
}
|
||||
|
||||
// New creates a new server.
|
||||
func New() *Server {
|
||||
return &Server{
|
||||
users: make(map[string]*User),
|
||||
}
|
||||
}
|
||||
|
||||
// NewSession creates a new IMAP session.
|
||||
func (s *Server) NewSession() imapserver.Session {
|
||||
return &serverSession{server: s}
|
||||
}
|
||||
|
||||
func (s *Server) user(username string) *User {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
return s.users[username]
|
||||
}
|
||||
|
||||
// AddUser adds a user to the server.
|
||||
func (s *Server) AddUser(user *User) {
|
||||
s.mutex.Lock()
|
||||
s.users[user.username] = user
|
||||
s.mutex.Unlock()
|
||||
}
|
||||
|
||||
type serverSession struct {
|
||||
*UserSession // may be nil
|
||||
|
||||
server *Server // immutable
|
||||
}
|
||||
|
||||
var _ imapserver.Session = (*serverSession)(nil)
|
||||
|
||||
func (sess *serverSession) Login(username, password string) error {
|
||||
u := sess.server.user(username)
|
||||
if u == nil {
|
||||
return imapserver.ErrAuthFailed
|
||||
}
|
||||
if err := u.Login(username, password); err != nil {
|
||||
return err
|
||||
}
|
||||
sess.UserSession = NewUserSession(u)
|
||||
return nil
|
||||
}
|
||||
140
imapserver/imapmemserver/session.go
Normal file
140
imapserver/imapmemserver/session.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
type (
|
||||
user = User
|
||||
mailbox = MailboxView
|
||||
)
|
||||
|
||||
// UserSession represents a session tied to a specific user.
|
||||
//
|
||||
// UserSession implements imapserver.Session. Typically, a UserSession pointer
|
||||
// is embedded into a larger struct which overrides Login.
|
||||
type UserSession struct {
|
||||
*user // immutable
|
||||
*mailbox // may be nil
|
||||
}
|
||||
|
||||
var _ imapserver.SessionIMAP4rev2 = (*UserSession)(nil)
|
||||
|
||||
// NewUserSession creates a new user session.
|
||||
func NewUserSession(user *User) *UserSession {
|
||||
return &UserSession{user: user}
|
||||
}
|
||||
|
||||
func (sess *UserSession) Close() error {
|
||||
if sess != nil && sess.mailbox != nil {
|
||||
sess.mailbox.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Select(name string, options *imap.SelectOptions) (*imap.SelectData, error) {
|
||||
mbox, err := sess.user.mailbox(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mbox.mutex.Lock()
|
||||
defer mbox.mutex.Unlock()
|
||||
sess.mailbox = mbox.NewView()
|
||||
return mbox.selectDataLocked(), nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Unselect() error {
|
||||
sess.mailbox.Close()
|
||||
sess.mailbox = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (*imap.CopyData, error) {
|
||||
dest, err := sess.user.mailbox(destName)
|
||||
if err != nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTryCreate,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Text: "Source and destination mailboxes are identical",
|
||||
}
|
||||
}
|
||||
|
||||
var sourceUIDs, destUIDs imap.UIDSet
|
||||
sess.mailbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||
appendData := dest.copyMsg(msg)
|
||||
sourceUIDs.AddNum(msg.uid)
|
||||
destUIDs.AddNum(appendData.UID)
|
||||
})
|
||||
|
||||
return &imap.CopyData{
|
||||
UIDValidity: dest.uidValidity,
|
||||
SourceUIDs: sourceUIDs,
|
||||
DestUIDs: destUIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) error {
|
||||
dest, err := sess.user.mailbox(destName)
|
||||
if err != nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTryCreate,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Text: "Source and destination mailboxes are identical",
|
||||
}
|
||||
}
|
||||
|
||||
sess.mailbox.mutex.Lock()
|
||||
defer sess.mailbox.mutex.Unlock()
|
||||
|
||||
var sourceUIDs, destUIDs imap.UIDSet
|
||||
expunged := make(map[*message]struct{})
|
||||
sess.mailbox.forEachLocked(numSet, func(seqNum uint32, msg *message) {
|
||||
appendData := dest.copyMsg(msg)
|
||||
sourceUIDs.AddNum(msg.uid)
|
||||
destUIDs.AddNum(appendData.UID)
|
||||
expunged[msg] = struct{}{}
|
||||
})
|
||||
seqNums := sess.mailbox.expungeLocked(expunged)
|
||||
|
||||
err = w.WriteCopyData(&imap.CopyData{
|
||||
UIDValidity: dest.uidValidity,
|
||||
SourceUIDs: sourceUIDs,
|
||||
DestUIDs: destUIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, seqNum := range seqNums {
|
||||
if err := w.WriteExpunge(sess.mailbox.tracker.EncodeSeqNum(seqNum)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *UserSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
||||
if sess.mailbox == nil {
|
||||
return nil
|
||||
}
|
||||
return sess.mailbox.Poll(w, allowExpunge)
|
||||
}
|
||||
|
||||
func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
||||
if sess.mailbox == nil {
|
||||
return nil // TODO
|
||||
}
|
||||
return sess.mailbox.Idle(w, stop)
|
||||
}
|
||||
204
imapserver/imapmemserver/user.go
Normal file
204
imapserver/imapmemserver/user.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package imapmemserver
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
const mailboxDelim rune = '/'
|
||||
|
||||
type User struct {
|
||||
username, password string
|
||||
|
||||
mutex sync.Mutex
|
||||
mailboxes map[string]*Mailbox
|
||||
prevUidValidity uint32
|
||||
}
|
||||
|
||||
func NewUser(username, password string) *User {
|
||||
return &User{
|
||||
username: username,
|
||||
password: password,
|
||||
mailboxes: make(map[string]*Mailbox),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) Login(username, password string) error {
|
||||
if username != u.username {
|
||||
return imapserver.ErrAuthFailed
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(u.password)) != 1 {
|
||||
return imapserver.ErrAuthFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) mailboxLocked(name string) (*Mailbox, error) {
|
||||
mbox := u.mailboxes[name]
|
||||
if mbox == nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeNonExistent,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
}
|
||||
return mbox, nil
|
||||
}
|
||||
|
||||
func (u *User) mailbox(name string) (*Mailbox, error) {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
return u.mailboxLocked(name)
|
||||
}
|
||||
|
||||
func (u *User) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) {
|
||||
mbox, err := u.mailbox(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mbox.StatusData(options), nil
|
||||
}
|
||||
|
||||
func (u *User) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
// TODO: fail if ref doesn't exist
|
||||
|
||||
if len(patterns) == 0 {
|
||||
return w.WriteList(&imap.ListData{
|
||||
Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect},
|
||||
Delim: mailboxDelim,
|
||||
})
|
||||
}
|
||||
|
||||
var l []imap.ListData
|
||||
for name, mbox := range u.mailboxes {
|
||||
match := false
|
||||
for _, pattern := range patterns {
|
||||
match = imapserver.MatchList(name, mailboxDelim, ref, pattern)
|
||||
if match {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
data := mbox.list(options)
|
||||
if data != nil {
|
||||
l = append(l, *data)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
return l[i].Mailbox < l[j].Mailbox
|
||||
})
|
||||
|
||||
for _, data := range l {
|
||||
if err := w.WriteList(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
|
||||
mbox, err := u.mailbox(mailbox)
|
||||
if err != nil {
|
||||
return nil, &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeTryCreate,
|
||||
Text: "No such mailbox",
|
||||
}
|
||||
}
|
||||
return mbox.appendLiteral(r, options)
|
||||
}
|
||||
|
||||
func (u *User) Create(name string, options *imap.CreateOptions) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
name = strings.TrimRight(name, string(mailboxDelim))
|
||||
|
||||
if u.mailboxes[name] != nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeAlreadyExists,
|
||||
Text: "Mailbox already exists",
|
||||
}
|
||||
}
|
||||
|
||||
// UIDVALIDITY must change if a mailbox is deleted and re-created with the
|
||||
// same name.
|
||||
u.prevUidValidity++
|
||||
u.mailboxes[name] = NewMailbox(name, u.prevUidValidity)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Delete(name string) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
if _, err := u.mailboxLocked(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(u.mailboxes, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Rename(oldName, newName string, options *imap.RenameOptions) error {
|
||||
u.mutex.Lock()
|
||||
defer u.mutex.Unlock()
|
||||
|
||||
newName = strings.TrimRight(newName, string(mailboxDelim))
|
||||
|
||||
mbox, err := u.mailboxLocked(oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.mailboxes[newName] != nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeAlreadyExists,
|
||||
Text: "Mailbox already exists",
|
||||
}
|
||||
}
|
||||
|
||||
mbox.rename(newName)
|
||||
u.mailboxes[newName] = mbox
|
||||
delete(u.mailboxes, oldName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Subscribe(name string) error {
|
||||
mbox, err := u.mailbox(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mbox.SetSubscribed(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Unsubscribe(name string) error {
|
||||
mbox, err := u.mailbox(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mbox.SetSubscribed(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Namespace() (*imap.NamespaceData, error) {
|
||||
return &imap.NamespaceData{
|
||||
Personal: []imap.NamespaceDescriptor{{Delim: mailboxDelim}},
|
||||
}, nil
|
||||
}
|
||||
329
imapserver/list.go
Normal file
329
imapserver/list.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||
)
|
||||
|
||||
func (c *Conn) handleList(dec *imapwire.Decoder) error {
|
||||
ref, pattern, options, err := readListCmd(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := &ListWriter{
|
||||
conn: c,
|
||||
options: options,
|
||||
}
|
||||
return c.session.List(w, ref, pattern, options)
|
||||
}
|
||||
|
||||
func (c *Conn) handleLSub(dec *imapwire.Decoder) error {
|
||||
var ref string
|
||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
pattern, err := readListMailbox(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := &imap.ListOptions{SelectSubscribed: true}
|
||||
w := &ListWriter{
|
||||
conn: c,
|
||||
lsub: true,
|
||||
}
|
||||
return c.session.List(w, ref, []string{pattern}, options)
|
||||
}
|
||||
|
||||
func (c *Conn) writeList(data *imap.ListData) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom("*").SP().Atom("LIST").SP()
|
||||
enc.List(len(data.Attrs), func(i int) {
|
||||
enc.MailboxAttr(data.Attrs[i])
|
||||
})
|
||||
enc.SP()
|
||||
if data.Delim == 0 {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.Quoted(string(data.Delim))
|
||||
}
|
||||
enc.SP().Mailbox(data.Mailbox)
|
||||
|
||||
var ext []string
|
||||
if data.ChildInfo != nil {
|
||||
ext = append(ext, "CHILDINFO")
|
||||
}
|
||||
if data.OldName != "" {
|
||||
ext = append(ext, "OLDNAME")
|
||||
}
|
||||
|
||||
// TODO: omit extended data if the client didn't ask for it
|
||||
if len(ext) > 0 {
|
||||
enc.SP().List(len(ext), func(i int) {
|
||||
name := ext[i]
|
||||
enc.Atom(name).SP()
|
||||
switch name {
|
||||
case "CHILDINFO":
|
||||
enc.Special('(')
|
||||
if data.ChildInfo.Subscribed {
|
||||
enc.Quoted("SUBSCRIBED")
|
||||
}
|
||||
enc.Special(')')
|
||||
case "OLDNAME":
|
||||
enc.Special('(').Mailbox(data.OldName).Special(')')
|
||||
default:
|
||||
panic(fmt.Errorf("imapserver: unknown LIST extended-item %v", name))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writeLSub(data *imap.ListData) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom("*").SP().Atom("LSUB").SP()
|
||||
enc.List(len(data.Attrs), func(i int) {
|
||||
enc.MailboxAttr(data.Attrs[i])
|
||||
})
|
||||
enc.SP()
|
||||
if data.Delim == 0 {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.Quoted(string(data.Delim))
|
||||
}
|
||||
enc.SP().Mailbox(data.Mailbox)
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func readListCmd(dec *imapwire.Decoder) (ref string, patterns []string, options *imap.ListOptions, err error) {
|
||||
options = &imap.ListOptions{}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return "", nil, nil, dec.Err()
|
||||
}
|
||||
|
||||
hasSelectOpts, err := dec.List(func() error {
|
||||
var selectOpt string
|
||||
if !dec.ExpectAString(&selectOpt) {
|
||||
return dec.Err()
|
||||
}
|
||||
switch strings.ToUpper(selectOpt) {
|
||||
case "SUBSCRIBED":
|
||||
options.SelectSubscribed = true
|
||||
case "REMOTE":
|
||||
options.SelectRemote = true
|
||||
case "RECURSIVEMATCH":
|
||||
options.SelectRecursiveMatch = true
|
||||
case "SPECIAL-USE":
|
||||
options.SelectSpecialUse = true
|
||||
default:
|
||||
return newClientBugError("Unknown LIST select option")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("in list-select-opts: %w", err)
|
||||
}
|
||||
if hasSelectOpts && !dec.ExpectSP() {
|
||||
return "", nil, nil, dec.Err()
|
||||
}
|
||||
|
||||
if !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
|
||||
return "", nil, nil, dec.Err()
|
||||
}
|
||||
|
||||
hasPatterns, err := dec.List(func() error {
|
||||
pattern, err := readListMailbox(dec)
|
||||
if err == nil && pattern != "" {
|
||||
patterns = append(patterns, pattern)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
} else if hasPatterns && len(patterns) == 0 {
|
||||
return "", nil, nil, newClientBugError("LIST-EXTENDED requires a non-empty parenthesized pattern list")
|
||||
} else if !hasPatterns {
|
||||
pattern, err := readListMailbox(dec)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if pattern != "" {
|
||||
patterns = append(patterns, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
if dec.SP() { // list-return-opts
|
||||
var atom string
|
||||
if !dec.ExpectAtom(&atom) || !dec.Expect(strings.EqualFold(atom, "RETURN"), "RETURN") || !dec.ExpectSP() {
|
||||
return "", nil, nil, dec.Err()
|
||||
}
|
||||
|
||||
err := dec.ExpectList(func() error {
|
||||
return readReturnOption(dec, options)
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("in list-return-opts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectCRLF() {
|
||||
return "", nil, nil, dec.Err()
|
||||
}
|
||||
|
||||
if options.SelectRecursiveMatch && !options.SelectSubscribed {
|
||||
return "", nil, nil, newClientBugError("The LIST RECURSIVEMATCH select option requires SUBSCRIBED")
|
||||
}
|
||||
|
||||
return ref, patterns, options, nil
|
||||
}
|
||||
|
||||
func readListMailbox(dec *imapwire.Decoder) (string, error) {
|
||||
var mailbox string
|
||||
if !dec.String(&mailbox) {
|
||||
if !dec.Expect(dec.Func(&mailbox, isListChar), "list-char") {
|
||||
return "", dec.Err()
|
||||
}
|
||||
}
|
||||
return utf7.Decode(mailbox)
|
||||
}
|
||||
|
||||
func isListChar(ch byte) bool {
|
||||
switch ch {
|
||||
case '%', '*': // list-wildcards
|
||||
return true
|
||||
case ']': // resp-specials
|
||||
return true
|
||||
default:
|
||||
return imapwire.IsAtomChar(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func readReturnOption(dec *imapwire.Decoder, options *imap.ListOptions) error {
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
switch strings.ToUpper(name) {
|
||||
case "SUBSCRIBED":
|
||||
options.ReturnSubscribed = true
|
||||
case "CHILDREN":
|
||||
options.ReturnChildren = true
|
||||
case "SPECIAL-USE":
|
||||
options.ReturnSpecialUse = true
|
||||
case "STATUS":
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
options.ReturnStatus = new(imap.StatusOptions)
|
||||
return dec.ExpectList(func() error {
|
||||
return readStatusItem(dec, options.ReturnStatus)
|
||||
})
|
||||
default:
|
||||
return newClientBugError("Unknown LIST RETURN options")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWriter writes LIST responses.
|
||||
type ListWriter struct {
|
||||
conn *Conn
|
||||
options *imap.ListOptions
|
||||
lsub bool
|
||||
}
|
||||
|
||||
// WriteList writes a single LIST response for a mailbox.
|
||||
func (w *ListWriter) WriteList(data *imap.ListData) error {
|
||||
if w.lsub {
|
||||
return w.conn.writeLSub(data)
|
||||
}
|
||||
|
||||
if err := w.conn.writeList(data); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.options.ReturnStatus != nil && data.Status != nil {
|
||||
if err := w.conn.writeStatus(data.Status, w.options.ReturnStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MatchList checks whether a reference and a pattern matches a mailbox.
|
||||
func MatchList(name string, delim rune, reference, pattern string) bool {
|
||||
var delimStr string
|
||||
if delim != 0 {
|
||||
delimStr = string(delim)
|
||||
}
|
||||
|
||||
if delimStr != "" && strings.HasPrefix(pattern, delimStr) {
|
||||
reference = ""
|
||||
pattern = strings.TrimPrefix(pattern, delimStr)
|
||||
}
|
||||
if reference != "" {
|
||||
if delimStr != "" && !strings.HasSuffix(reference, delimStr) {
|
||||
reference += delimStr
|
||||
}
|
||||
if !strings.HasPrefix(name, reference) {
|
||||
return false
|
||||
}
|
||||
name = strings.TrimPrefix(name, reference)
|
||||
}
|
||||
|
||||
return matchList(name, delimStr, pattern)
|
||||
}
|
||||
|
||||
func matchList(name, delim, pattern string) bool {
|
||||
// TODO: optimize
|
||||
|
||||
i := strings.IndexAny(pattern, "*%")
|
||||
if i == -1 {
|
||||
// No more wildcards
|
||||
return name == pattern
|
||||
}
|
||||
|
||||
// Get parts before and after wildcard
|
||||
chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]
|
||||
|
||||
// Check that name begins with chunk
|
||||
if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
|
||||
return false
|
||||
}
|
||||
name = strings.TrimPrefix(name, chunk)
|
||||
|
||||
// Expand wildcard
|
||||
var j int
|
||||
for j = 0; j < len(name); j++ {
|
||||
if wildcard == '%' && string(name[j]) == delim {
|
||||
break // Stop on delimiter if wildcard is %
|
||||
}
|
||||
// Try to match the rest from here
|
||||
if matchList(name[j:], delim, rest) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return matchList(name[j:], delim, rest)
|
||||
}
|
||||
51
imapserver/list_test.go
Normal file
51
imapserver/list_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package imapserver_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
var matchListTests = []struct {
|
||||
name, ref, pattern string
|
||||
result bool
|
||||
}{
|
||||
{name: "INBOX", pattern: "INBOX", result: true},
|
||||
{name: "INBOX", pattern: "Asuka", result: false},
|
||||
{name: "INBOX", pattern: "*", result: true},
|
||||
{name: "INBOX", pattern: "%", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "*", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "%", result: false},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/*", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/%", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "Neo* Evangelion/Misato", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "Neo% Evangelion/Misato", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "*Eva*/Misato", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "%Eva%/Misato", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "*X*/Misato", result: false},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "%X%/Misato", result: false},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%o", result: true},
|
||||
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%too", result: false},
|
||||
{name: "Misato/Misato", pattern: "Mis*to/Misato", result: true},
|
||||
{name: "Misato/Misato", pattern: "Mis*to", result: true},
|
||||
{name: "Misato/Misato/Misato", pattern: "Mis*to/Mis%to", result: true},
|
||||
{name: "Misato/Misato", pattern: "Mis**to/Misato", result: true},
|
||||
{name: "Misato/Misato", pattern: "Misat%/Misato", result: true},
|
||||
{name: "Misato/Misato", pattern: "Misat%Misato", result: false},
|
||||
{name: "Misato/Misato", ref: "Misato", pattern: "Misato", result: true},
|
||||
{name: "Misato/Misato", ref: "Misato/", pattern: "Misato", result: true},
|
||||
{name: "Misato/Misato", ref: "Shinji", pattern: "/Misato/*", result: true},
|
||||
{name: "Misato/Misato", ref: "Misato", pattern: "/Misato", result: false},
|
||||
{name: "Misato/Misato", ref: "Misato", pattern: "Shinji", result: false},
|
||||
{name: "Misato/Misato", ref: "Shinji", pattern: "Misato", result: false},
|
||||
}
|
||||
|
||||
func TestMatchList(t *testing.T) {
|
||||
delim := '/'
|
||||
for _, test := range matchListTests {
|
||||
result := imapserver.MatchList(test.name, delim, test.ref, test.pattern)
|
||||
if result != test.result {
|
||||
t.Errorf("matching name %q with pattern %q and reference %q returns %v, but expected %v", test.name, test.pattern, test.ref, result, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
imapserver/login.go
Normal file
28
imapserver/login.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleLogin(tag string, dec *imapwire.Decoder) error {
|
||||
var username, password string
|
||||
if !dec.ExpectSP() || !dec.ExpectAString(&username) || !dec.ExpectSP() || !dec.ExpectAString(&password) || !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
if err := c.checkState(imap.ConnStateNotAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
if !c.canAuth() {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodePrivacyRequired,
|
||||
Text: "TLS is required to authenticate",
|
||||
}
|
||||
}
|
||||
if err := c.session.Login(username, password); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = imap.ConnStateAuthenticated
|
||||
return c.writeCapabilityStatus(tag, imap.StatusResponseTypeOK, "Logged in")
|
||||
}
|
||||
336
imapserver/message.go
Normal file
336
imapserver/message.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
gomessage "github.com/emersion/go-message"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-message/textproto"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// ExtractBodySection extracts a section of a message body.
|
||||
//
|
||||
// It can be used by server backends to implement Session.Fetch.
|
||||
func ExtractBodySection(r io.Reader, item *imap.FetchItemBodySection) []byte {
|
||||
var (
|
||||
header textproto.Header
|
||||
body io.Reader
|
||||
)
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
header, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
body = br
|
||||
|
||||
parentMediaType, header, body := findMessagePart(header, body, item.Part)
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(item.Part) > 0 {
|
||||
switch item.Specifier {
|
||||
case imap.PartSpecifierHeader, imap.PartSpecifierText:
|
||||
header, body = openMessagePart(header, body, parentMediaType)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter header fields
|
||||
if len(item.HeaderFields) > 0 {
|
||||
keep := make(map[string]struct{})
|
||||
for _, k := range item.HeaderFields {
|
||||
keep[strings.ToLower(k)] = struct{}{}
|
||||
}
|
||||
for field := header.Fields(); field.Next(); {
|
||||
if _, ok := keep[strings.ToLower(field.Key())]; !ok {
|
||||
field.Del()
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, k := range item.HeaderFieldsNot {
|
||||
header.Del(k)
|
||||
}
|
||||
|
||||
// Write the requested data to a buffer
|
||||
var buf bytes.Buffer
|
||||
|
||||
writeHeader := true
|
||||
switch item.Specifier {
|
||||
case imap.PartSpecifierNone:
|
||||
writeHeader = len(item.Part) == 0
|
||||
case imap.PartSpecifierText:
|
||||
writeHeader = false
|
||||
}
|
||||
if writeHeader {
|
||||
if err := textproto.WriteHeader(&buf, header); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch item.Specifier {
|
||||
case imap.PartSpecifierNone, imap.PartSpecifierText:
|
||||
if _, err := io.Copy(&buf, body); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return extractPartial(buf.Bytes(), item.Partial)
|
||||
}
|
||||
|
||||
func findMessagePart(header textproto.Header, body io.Reader, partPath []int) (string, textproto.Header, io.Reader) {
|
||||
// First part of non-multipart message refers to the message itself
|
||||
msgHeader := gomessage.Header{header}
|
||||
mediaType, _, _ := msgHeader.ContentType()
|
||||
if !strings.HasPrefix(mediaType, "multipart/") && len(partPath) > 0 && partPath[0] == 1 {
|
||||
partPath = partPath[1:]
|
||||
}
|
||||
|
||||
var parentMediaType string
|
||||
for i := 0; i < len(partPath); i++ {
|
||||
partNum := partPath[i]
|
||||
|
||||
header, body = openMessagePart(header, body, parentMediaType)
|
||||
|
||||
msgHeader := gomessage.Header{header}
|
||||
mediaType, typeParams, _ := msgHeader.ContentType()
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
if partNum != 1 {
|
||||
return "", textproto.Header{}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
mr := textproto.NewMultipartReader(body, typeParams["boundary"])
|
||||
found := false
|
||||
for j := 1; j <= partNum; j++ {
|
||||
p, err := mr.NextPart()
|
||||
if err != nil {
|
||||
return "", textproto.Header{}, nil
|
||||
}
|
||||
|
||||
if j == partNum {
|
||||
parentMediaType = mediaType
|
||||
header = p.Header
|
||||
body = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "", textproto.Header{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return parentMediaType, header, body
|
||||
}
|
||||
|
||||
func openMessagePart(header textproto.Header, body io.Reader, parentMediaType string) (textproto.Header, io.Reader) {
|
||||
msgHeader := gomessage.Header{header}
|
||||
mediaType, _, _ := msgHeader.ContentType()
|
||||
if !msgHeader.Has("Content-Type") && parentMediaType == "multipart/digest" {
|
||||
mediaType = "message/rfc822"
|
||||
}
|
||||
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
||||
br := bufio.NewReader(body)
|
||||
header, _ = textproto.ReadHeader(br)
|
||||
return header, br
|
||||
}
|
||||
return header, body
|
||||
}
|
||||
|
||||
func extractPartial(b []byte, partial *imap.SectionPartial) []byte {
|
||||
if partial == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
end := partial.Offset + partial.Size
|
||||
if partial.Offset > int64(len(b)) {
|
||||
return nil
|
||||
}
|
||||
if end > int64(len(b)) {
|
||||
end = int64(len(b))
|
||||
}
|
||||
return b[partial.Offset:end]
|
||||
}
|
||||
|
||||
func ExtractBinarySection(r io.Reader, item *imap.FetchItemBinarySection) []byte {
|
||||
var (
|
||||
header textproto.Header
|
||||
body io.Reader
|
||||
)
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
header, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
body = br
|
||||
|
||||
_, header, body = findMessagePart(header, body, item.Part)
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
part, err := gomessage.New(gomessage.Header{header}, body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write the requested data to a buffer
|
||||
var buf bytes.Buffer
|
||||
|
||||
if len(item.Part) == 0 {
|
||||
if err := textproto.WriteHeader(&buf, part.Header.Header); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := io.Copy(&buf, part.Body); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return extractPartial(buf.Bytes(), item.Partial)
|
||||
}
|
||||
|
||||
func ExtractBinarySectionSize(r io.Reader, item *imap.FetchItemBinarySectionSize) uint32 {
|
||||
// TODO: optimize
|
||||
b := ExtractBinarySection(r, &imap.FetchItemBinarySection{Part: item.Part})
|
||||
return uint32(len(b))
|
||||
}
|
||||
|
||||
// ExtractEnvelope returns a message envelope from its header.
|
||||
//
|
||||
// It can be used by server backends to implement Session.Fetch.
|
||||
func ExtractEnvelope(h textproto.Header) *imap.Envelope {
|
||||
mh := mail.Header{gomessage.Header{h}}
|
||||
date, _ := mh.Date()
|
||||
subject, _ := mh.Subject()
|
||||
inReplyTo, _ := mh.MsgIDList("In-Reply-To")
|
||||
messageID, _ := mh.MessageID()
|
||||
return &imap.Envelope{
|
||||
Date: date,
|
||||
Subject: subject,
|
||||
From: parseAddressList(mh, "From"),
|
||||
Sender: parseAddressList(mh, "Sender"),
|
||||
ReplyTo: parseAddressList(mh, "Reply-To"),
|
||||
To: parseAddressList(mh, "To"),
|
||||
Cc: parseAddressList(mh, "Cc"),
|
||||
Bcc: parseAddressList(mh, "Bcc"),
|
||||
InReplyTo: inReplyTo,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
|
||||
func parseAddressList(mh mail.Header, k string) []imap.Address {
|
||||
// TODO: handle groups
|
||||
addrs, _ := mh.AddressList(k)
|
||||
var l []imap.Address
|
||||
for _, addr := range addrs {
|
||||
mailbox, host, ok := strings.Cut(addr.Address, "@")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
l = append(l, imap.Address{
|
||||
Name: addr.Name,
|
||||
Mailbox: mailbox,
|
||||
Host: host,
|
||||
})
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// ExtractBodyStructure extracts the structure of a message body.
|
||||
//
|
||||
// It can be used by server backends to implement Session.Fetch.
|
||||
func ExtractBodyStructure(r io.Reader) imap.BodyStructure {
|
||||
br := bufio.NewReader(r)
|
||||
header, _ := textproto.ReadHeader(br)
|
||||
return extractBodyStructure(header, br)
|
||||
}
|
||||
|
||||
func extractBodyStructure(rawHeader textproto.Header, r io.Reader) imap.BodyStructure {
|
||||
header := gomessage.Header{rawHeader}
|
||||
|
||||
mediaType, typeParams, _ := header.ContentType()
|
||||
primaryType, subType, _ := strings.Cut(mediaType, "/")
|
||||
|
||||
if primaryType == "multipart" {
|
||||
bs := &imap.BodyStructureMultiPart{Subtype: subType}
|
||||
mr := textproto.NewMultipartReader(r, typeParams["boundary"])
|
||||
for {
|
||||
part, _ := mr.NextPart()
|
||||
if part == nil {
|
||||
break
|
||||
}
|
||||
bs.Children = append(bs.Children, extractBodyStructure(part.Header, part))
|
||||
}
|
||||
bs.Extended = &imap.BodyStructureMultiPartExt{
|
||||
Params: typeParams,
|
||||
Disposition: getContentDisposition(header),
|
||||
Language: getContentLanguage(header),
|
||||
Location: header.Get("Content-Location"),
|
||||
}
|
||||
return bs
|
||||
} else {
|
||||
body, _ := io.ReadAll(r) // TODO: optimize
|
||||
bs := &imap.BodyStructureSinglePart{
|
||||
Type: primaryType,
|
||||
Subtype: subType,
|
||||
Params: typeParams,
|
||||
ID: header.Get("Content-Id"),
|
||||
Description: header.Get("Content-Description"),
|
||||
Encoding: header.Get("Content-Transfer-Encoding"),
|
||||
Size: uint32(len(body)),
|
||||
}
|
||||
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
||||
br := bufio.NewReader(bytes.NewReader(body))
|
||||
childHeader, _ := textproto.ReadHeader(br)
|
||||
bs.MessageRFC822 = &imap.BodyStructureMessageRFC822{
|
||||
Envelope: ExtractEnvelope(childHeader),
|
||||
BodyStructure: extractBodyStructure(childHeader, br),
|
||||
NumLines: int64(bytes.Count(body, []byte("\n"))),
|
||||
}
|
||||
}
|
||||
if primaryType == "text" {
|
||||
bs.Text = &imap.BodyStructureText{
|
||||
NumLines: int64(bytes.Count(body, []byte("\n"))),
|
||||
}
|
||||
}
|
||||
bs.Extended = &imap.BodyStructureSinglePartExt{
|
||||
Disposition: getContentDisposition(header),
|
||||
Language: getContentLanguage(header),
|
||||
Location: header.Get("Content-Location"),
|
||||
}
|
||||
return bs
|
||||
}
|
||||
}
|
||||
|
||||
func getContentDisposition(header gomessage.Header) *imap.BodyStructureDisposition {
|
||||
disp, dispParams, _ := header.ContentDisposition()
|
||||
if disp == "" {
|
||||
return nil
|
||||
}
|
||||
return &imap.BodyStructureDisposition{
|
||||
Value: disp,
|
||||
Params: dispParams,
|
||||
}
|
||||
}
|
||||
|
||||
func getContentLanguage(header gomessage.Header) []string {
|
||||
v := header.Get("Content-Language")
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
// TODO: handle CFWS
|
||||
l := strings.Split(v, ",")
|
||||
for i, lang := range l {
|
||||
l[i] = strings.TrimSpace(lang)
|
||||
}
|
||||
return l
|
||||
}
|
||||
40
imapserver/move.go
Normal file
40
imapserver/move.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleMove(dec *imapwire.Decoder, numKind NumKind) error {
|
||||
numSet, dest, err := readCopy(numKind, dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
session, ok := c.session.(SessionMove)
|
||||
if !ok {
|
||||
return newClientBugError("MOVE is not supported")
|
||||
}
|
||||
w := &MoveWriter{conn: c}
|
||||
return session.Move(w, numSet, dest)
|
||||
}
|
||||
|
||||
// MoveWriter writes responses for the MOVE command.
|
||||
//
|
||||
// Servers must first call WriteCopyData once, then call WriteExpunge any
|
||||
// number of times.
|
||||
type MoveWriter struct {
|
||||
conn *Conn
|
||||
}
|
||||
|
||||
// WriteCopyData writes the untagged COPYUID response for a MOVE command.
|
||||
func (w *MoveWriter) WriteCopyData(data *imap.CopyData) error {
|
||||
return w.conn.writeCopyOK("", data)
|
||||
}
|
||||
|
||||
// WriteExpunge writes an EXPUNGE response for a MOVE command.
|
||||
func (w *MoveWriter) WriteExpunge(seqNum uint32) error {
|
||||
return w.conn.writeExpunge(seqNum)
|
||||
}
|
||||
54
imapserver/namespace.go
Normal file
54
imapserver/namespace.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleNamespace(dec *imapwire.Decoder) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, ok := c.session.(SessionNamespace)
|
||||
if !ok {
|
||||
return newClientBugError("NAMESPACE is not supported")
|
||||
}
|
||||
|
||||
data, err := session.Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("NAMESPACE").SP()
|
||||
writeNamespace(enc.Encoder, data.Personal)
|
||||
enc.SP()
|
||||
writeNamespace(enc.Encoder, data.Other)
|
||||
enc.SP()
|
||||
writeNamespace(enc.Encoder, data.Shared)
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func writeNamespace(enc *imapwire.Encoder, l []imap.NamespaceDescriptor) {
|
||||
if l == nil {
|
||||
enc.NIL()
|
||||
return
|
||||
}
|
||||
|
||||
enc.List(len(l), func(i int) {
|
||||
descr := l[i]
|
||||
enc.Special('(').String(descr.Prefix).SP()
|
||||
if descr.Delim == 0 {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.Quoted(string(descr.Delim))
|
||||
}
|
||||
enc.Special(')')
|
||||
})
|
||||
}
|
||||
343
imapserver/search.go
Normal file
343
imapserver/search.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleSearch(tag string, dec *imapwire.Decoder, numKind NumKind) error {
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
var (
|
||||
atom string
|
||||
options imap.SearchOptions
|
||||
extended bool
|
||||
)
|
||||
if maybeReadSearchKeyAtom(dec, &atom) && strings.EqualFold(atom, "RETURN") {
|
||||
if err := readSearchReturnOpts(dec, &options); err != nil {
|
||||
return fmt.Errorf("in search-return-opts: %w", err)
|
||||
}
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
extended = true
|
||||
atom = ""
|
||||
maybeReadSearchKeyAtom(dec, &atom)
|
||||
}
|
||||
if strings.EqualFold(atom, "CHARSET") {
|
||||
var charset string
|
||||
if !dec.ExpectSP() || !dec.ExpectAString(&charset) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
switch strings.ToUpper(charset) {
|
||||
case "US-ASCII", "UTF-8":
|
||||
// nothing to do
|
||||
default:
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeBadCharset, // TODO: return list of supported charsets
|
||||
Text: "Only US-ASCII and UTF-8 are supported SEARCH charsets",
|
||||
}
|
||||
}
|
||||
atom = ""
|
||||
maybeReadSearchKeyAtom(dec, &atom)
|
||||
}
|
||||
|
||||
var criteria imap.SearchCriteria
|
||||
for {
|
||||
var err error
|
||||
if atom != "" {
|
||||
err = readSearchKeyWithAtom(&criteria, dec, atom)
|
||||
atom = ""
|
||||
} else {
|
||||
err = readSearchKey(&criteria, dec)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("in search-key: %w", err)
|
||||
}
|
||||
|
||||
if !dec.SP() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If no return option is specified, ALL is assumed
|
||||
if !options.ReturnMin && !options.ReturnMax && !options.ReturnAll && !options.ReturnCount {
|
||||
options.ReturnAll = true
|
||||
}
|
||||
|
||||
data, err := c.session.Search(numKind, &criteria, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.enabled.Has(imap.CapIMAP4rev2) || extended {
|
||||
return c.writeESearch(tag, data, &options, numKind)
|
||||
} else {
|
||||
return c.writeSearch(data.All)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.SearchOptions, numKind NumKind) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom("*").SP().Atom("ESEARCH")
|
||||
if tag != "" {
|
||||
enc.SP().Special('(').Atom("TAG").SP().String(tag).Special(')')
|
||||
}
|
||||
if numKind == NumKindUID {
|
||||
enc.SP().Atom("UID")
|
||||
}
|
||||
// When there is no result, we need to send an ESEARCH response with no ALL
|
||||
// keyword
|
||||
if options.ReturnAll && !isNumSetEmpty(data.All) {
|
||||
enc.SP().Atom("ALL").SP().NumSet(data.All)
|
||||
}
|
||||
if options.ReturnMin && data.Min > 0 {
|
||||
enc.SP().Atom("MIN").SP().Number(data.Min)
|
||||
}
|
||||
if options.ReturnMax && data.Max > 0 {
|
||||
enc.SP().Atom("MAX").SP().Number(data.Max)
|
||||
}
|
||||
if options.ReturnCount {
|
||||
enc.SP().Atom("COUNT").SP().Number(data.Count)
|
||||
}
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func isNumSetEmpty(numSet imap.NumSet) bool {
|
||||
switch numSet := numSet.(type) {
|
||||
case imap.SeqSet:
|
||||
return len(numSet) == 0
|
||||
case imap.UIDSet:
|
||||
return len(numSet) == 0
|
||||
default:
|
||||
panic("unknown imap.NumSet type")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) writeSearch(numSet imap.NumSet) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom("*").SP().Atom("SEARCH")
|
||||
var ok bool
|
||||
switch numSet := numSet.(type) {
|
||||
case imap.SeqSet:
|
||||
var nums []uint32
|
||||
nums, ok = numSet.Nums()
|
||||
for _, num := range nums {
|
||||
enc.SP().Number(num)
|
||||
}
|
||||
case imap.UIDSet:
|
||||
var uids []imap.UID
|
||||
uids, ok = numSet.Nums()
|
||||
for _, uid := range uids {
|
||||
enc.SP().UID(uid)
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("imapserver: failed to enumerate message numbers in SEARCH response")
|
||||
}
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func readSearchReturnOpts(dec *imapwire.Decoder, options *imap.SearchOptions) error {
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
return dec.ExpectList(func() error {
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return dec.Err()
|
||||
}
|
||||
switch strings.ToUpper(name) {
|
||||
case "MIN":
|
||||
options.ReturnMin = true
|
||||
case "MAX":
|
||||
options.ReturnMax = true
|
||||
case "ALL":
|
||||
options.ReturnAll = true
|
||||
case "COUNT":
|
||||
options.ReturnCount = true
|
||||
case "SAVE":
|
||||
options.ReturnSave = true
|
||||
default:
|
||||
return newClientBugError("unknown SEARCH RETURN option")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func maybeReadSearchKeyAtom(dec *imapwire.Decoder, ptr *string) bool {
|
||||
return dec.Func(ptr, func(ch byte) bool {
|
||||
return ch == '*' || imapwire.IsAtomChar(ch)
|
||||
})
|
||||
}
|
||||
|
||||
func readSearchKey(criteria *imap.SearchCriteria, dec *imapwire.Decoder) error {
|
||||
var key string
|
||||
if maybeReadSearchKeyAtom(dec, &key) {
|
||||
return readSearchKeyWithAtom(criteria, dec, key)
|
||||
}
|
||||
return dec.ExpectList(func() error {
|
||||
return readSearchKey(criteria, dec)
|
||||
})
|
||||
}
|
||||
|
||||
func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder, key string) error {
|
||||
key = strings.ToUpper(key)
|
||||
switch key {
|
||||
case "ALL":
|
||||
// nothing to do
|
||||
case "UID":
|
||||
var uidSet imap.UIDSet
|
||||
if !dec.ExpectSP() || !dec.ExpectUIDSet(&uidSet) {
|
||||
return dec.Err()
|
||||
}
|
||||
criteria.UID = append(criteria.UID, uidSet)
|
||||
case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN":
|
||||
criteria.Flag = append(criteria.Flag, searchKeyFlag(key))
|
||||
case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN":
|
||||
notKey := strings.TrimPrefix(key, "UN")
|
||||
criteria.NotFlag = append(criteria.NotFlag, searchKeyFlag(notKey))
|
||||
case "NEW":
|
||||
criteria.Flag = append(criteria.Flag, internal.FlagRecent)
|
||||
criteria.NotFlag = append(criteria.NotFlag, imap.FlagSeen)
|
||||
case "OLD":
|
||||
criteria.NotFlag = append(criteria.NotFlag, internal.FlagRecent)
|
||||
case "KEYWORD", "UNKEYWORD":
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
flag, err := internal.ExpectFlag(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case "KEYWORD":
|
||||
criteria.Flag = append(criteria.Flag, flag)
|
||||
case "UNKEYWORD":
|
||||
criteria.NotFlag = append(criteria.NotFlag, flag)
|
||||
}
|
||||
case "BCC", "CC", "FROM", "SUBJECT", "TO":
|
||||
var value string
|
||||
if !dec.ExpectSP() || !dec.ExpectAString(&value) {
|
||||
return dec.Err()
|
||||
}
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: strings.Title(strings.ToLower(key)),
|
||||
Value: value,
|
||||
})
|
||||
case "HEADER":
|
||||
var key, value string
|
||||
if !dec.ExpectSP() || !dec.ExpectAString(&key) || !dec.ExpectSP() || !dec.ExpectAString(&value) {
|
||||
return dec.Err()
|
||||
}
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
case "SINCE", "BEFORE", "ON", "SENTSINCE", "SENTBEFORE", "SENTON":
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
t, err := internal.ExpectDate(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var dateCriteria imap.SearchCriteria
|
||||
switch key {
|
||||
case "SINCE":
|
||||
dateCriteria.Since = t
|
||||
case "BEFORE":
|
||||
dateCriteria.Before = t
|
||||
case "ON":
|
||||
dateCriteria.Since = t
|
||||
dateCriteria.Before = t.Add(24 * time.Hour)
|
||||
case "SENTSINCE":
|
||||
dateCriteria.SentSince = t
|
||||
case "SENTBEFORE":
|
||||
dateCriteria.SentBefore = t
|
||||
case "SENTON":
|
||||
dateCriteria.SentSince = t
|
||||
dateCriteria.SentBefore = t.Add(24 * time.Hour)
|
||||
}
|
||||
criteria.And(&dateCriteria)
|
||||
case "BODY":
|
||||
var body string
|
||||
if !dec.ExpectSP() || !dec.ExpectAString(&body) {
|
||||
return dec.Err()
|
||||
}
|
||||
criteria.Body = append(criteria.Body, body)
|
||||
case "TEXT":
|
||||
var text string
|
||||
if !dec.ExpectSP() || !dec.ExpectAString(&text) {
|
||||
return dec.Err()
|
||||
}
|
||||
criteria.Text = append(criteria.Text, text)
|
||||
case "LARGER", "SMALLER":
|
||||
var n int64
|
||||
if !dec.ExpectSP() || !dec.ExpectNumber64(&n) {
|
||||
return dec.Err()
|
||||
}
|
||||
switch key {
|
||||
case "LARGER":
|
||||
criteria.And(&imap.SearchCriteria{Larger: n})
|
||||
case "SMALLER":
|
||||
criteria.And(&imap.SearchCriteria{Smaller: n})
|
||||
}
|
||||
case "NOT":
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
var not imap.SearchCriteria
|
||||
if err := readSearchKey(¬, dec); err != nil {
|
||||
return err
|
||||
}
|
||||
criteria.Not = append(criteria.Not, not)
|
||||
case "OR":
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
var or [2]imap.SearchCriteria
|
||||
if err := readSearchKey(&or[0], dec); err != nil {
|
||||
return err
|
||||
}
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
if err := readSearchKey(&or[1], dec); err != nil {
|
||||
return err
|
||||
}
|
||||
criteria.Or = append(criteria.Or, or)
|
||||
case "$":
|
||||
criteria.UID = append(criteria.UID, imap.SearchRes())
|
||||
default:
|
||||
seqSet, err := imapwire.ParseSeqSet(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
criteria.SeqNum = append(criteria.SeqNum, seqSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchKeyFlag(key string) imap.Flag {
|
||||
return imap.Flag("\\" + strings.Title(strings.ToLower(key)))
|
||||
}
|
||||
174
imapserver/select.go
Normal file
174
imapserver/select.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleSelect(tag string, dec *imapwire.Decoder, readOnly bool) error {
|
||||
var mailbox string
|
||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&mailbox) || !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.state == imap.ConnStateSelected {
|
||||
if err := c.session.Unselect(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = imap.ConnStateAuthenticated
|
||||
err := c.writeStatusResp("", &imap.StatusResponse{
|
||||
Type: imap.StatusResponseTypeOK,
|
||||
Code: "CLOSED",
|
||||
Text: "Previous mailbox is now closed",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
options := imap.SelectOptions{ReadOnly: readOnly}
|
||||
data, err := c.session.Select(mailbox, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.writeExists(data.NumMessages); err != nil {
|
||||
return err
|
||||
}
|
||||
if !c.enabled.Has(imap.CapIMAP4rev2) && c.server.options.caps().Has(imap.CapIMAP4rev1) {
|
||||
if err := c.writeObsoleteRecent(data.NumRecent); err != nil {
|
||||
return err
|
||||
}
|
||||
if data.FirstUnseenSeqNum != 0 {
|
||||
if err := c.writeObsoleteUnseen(data.FirstUnseenSeqNum); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := c.writeUIDValidity(data.UIDValidity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeUIDNext(data.UIDNext); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeFlags(data.Flags); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePermanentFlags(data.PermanentFlags); err != nil {
|
||||
return err
|
||||
}
|
||||
if data.List != nil {
|
||||
if err := c.writeList(data.List); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.state = imap.ConnStateSelected
|
||||
// TODO: forbid write commands in read-only mode
|
||||
|
||||
var (
|
||||
cmdName string
|
||||
code imap.ResponseCode
|
||||
)
|
||||
if readOnly {
|
||||
cmdName = "EXAMINE"
|
||||
code = "READ-ONLY"
|
||||
} else {
|
||||
cmdName = "SELECT"
|
||||
code = "READ-WRITE"
|
||||
}
|
||||
return c.writeStatusResp(tag, &imap.StatusResponse{
|
||||
Type: imap.StatusResponseTypeOK,
|
||||
Code: code,
|
||||
Text: fmt.Sprintf("%v completed", cmdName),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) handleUnselect(dec *imapwire.Decoder, expunge bool) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if expunge {
|
||||
w := &ExpungeWriter{}
|
||||
if err := c.session.Expunge(w, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.session.Unselect(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.state = imap.ConnStateAuthenticated
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeExists(numMessages uint32) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
return enc.Atom("*").SP().Number(numMessages).SP().Atom("EXISTS").CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writeObsoleteRecent(n uint32) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
return enc.Atom("*").SP().Number(n).SP().Atom("RECENT").CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writeObsoleteUnseen(n uint32) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("OK").SP()
|
||||
enc.Special('[').Atom("UNSEEN").SP().Number(n).Special(']')
|
||||
enc.SP().Text("First unseen message")
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writeUIDValidity(uidValidity uint32) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("OK").SP()
|
||||
enc.Special('[').Atom("UIDVALIDITY").SP().Number(uidValidity).Special(']')
|
||||
enc.SP().Text("UIDs valid")
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writeUIDNext(uidNext imap.UID) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("OK").SP()
|
||||
enc.Special('[').Atom("UIDNEXT").SP().UID(uidNext).Special(']')
|
||||
enc.SP().Text("Predicted next UID")
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writeFlags(flags []imap.Flag) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("FLAGS").SP().List(len(flags), func(i int) {
|
||||
enc.Flag(flags[i])
|
||||
})
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func (c *Conn) writePermanentFlags(flags []imap.Flag) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
enc.Atom("*").SP().Atom("OK").SP()
|
||||
enc.Special('[').Atom("PERMANENTFLAGS").SP().List(len(flags), func(i int) {
|
||||
enc.Flag(flags[i])
|
||||
}).Special(']')
|
||||
enc.SP().Text("Permanent flags")
|
||||
return enc.CRLF()
|
||||
}
|
||||
222
imapserver/server.go
Normal file
222
imapserver/server.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Package imapserver implements an IMAP server.
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
var errClosed = errors.New("imapserver: server closed")
|
||||
|
||||
// Logger is a facility to log error messages.
|
||||
type Logger interface {
|
||||
Printf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// Options contains server options.
|
||||
//
|
||||
// The only required field is NewSession.
|
||||
type Options struct {
|
||||
// NewSession is called when a client connects.
|
||||
NewSession func(*Conn) (Session, *GreetingData, error)
|
||||
// Supported capabilities. If nil, only IMAP4rev1 is advertised. This set
|
||||
// must contain at least IMAP4rev1 or IMAP4rev2.
|
||||
//
|
||||
// The following capabilities are part of IMAP4rev2 and need to be
|
||||
// explicitly enabled by IMAP4rev1-only servers:
|
||||
//
|
||||
// - NAMESPACE
|
||||
// - UIDPLUS
|
||||
// - ESEARCH
|
||||
// - LIST-EXTENDED
|
||||
// - LIST-STATUS
|
||||
// - MOVE
|
||||
// - STATUS=SIZE
|
||||
Caps imap.CapSet
|
||||
// Logger is a logger to print error messages. If nil, log.Default is used.
|
||||
Logger Logger
|
||||
// TLSConfig is a TLS configuration for STARTTLS. If nil, STARTTLS is
|
||||
// disabled.
|
||||
TLSConfig *tls.Config
|
||||
// InsecureAuth allows clients to authenticate without TLS. In this mode,
|
||||
// the server is susceptible to man-in-the-middle attacks.
|
||||
InsecureAuth bool
|
||||
// Raw ingress and egress data will be written to this writer, if any.
|
||||
// Note, this may include sensitive information such as credentials used
|
||||
// during authentication.
|
||||
DebugWriter io.Writer
|
||||
}
|
||||
|
||||
func (options *Options) wrapReadWriter(rw io.ReadWriter) io.ReadWriter {
|
||||
if options.DebugWriter == nil {
|
||||
return rw
|
||||
}
|
||||
return struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}{
|
||||
Reader: io.TeeReader(rw, options.DebugWriter),
|
||||
Writer: io.MultiWriter(rw, options.DebugWriter),
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Options) caps() imap.CapSet {
|
||||
if options.Caps != nil {
|
||||
return options.Caps
|
||||
}
|
||||
return imap.CapSet{imap.CapIMAP4rev1: {}}
|
||||
}
|
||||
|
||||
// Server is an IMAP server.
|
||||
type Server struct {
|
||||
options Options
|
||||
|
||||
listenerWaitGroup sync.WaitGroup
|
||||
|
||||
mutex sync.Mutex
|
||||
listeners map[net.Listener]struct{}
|
||||
conns map[*Conn]struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
// New creates a new server.
|
||||
func New(options *Options) *Server {
|
||||
if caps := options.caps(); !caps.Has(imap.CapIMAP4rev2) && !caps.Has(imap.CapIMAP4rev1) {
|
||||
panic("imapserver: at least IMAP4rev1 must be supported")
|
||||
}
|
||||
return &Server{
|
||||
options: *options,
|
||||
listeners: make(map[net.Listener]struct{}),
|
||||
conns: make(map[*Conn]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) logger() Logger {
|
||||
if s.options.Logger == nil {
|
||||
return log.Default()
|
||||
}
|
||||
return s.options.Logger
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the listener ln.
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
s.mutex.Lock()
|
||||
ok := !s.closed
|
||||
if ok {
|
||||
s.listeners[ln] = struct{}{}
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
if !ok {
|
||||
return errClosed
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.mutex.Lock()
|
||||
delete(s.listeners, ln)
|
||||
s.mutex.Unlock()
|
||||
}()
|
||||
|
||||
s.listenerWaitGroup.Add(1)
|
||||
defer s.listenerWaitGroup.Done()
|
||||
|
||||
var delay time.Duration
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||
if delay == 0 {
|
||||
delay = 5 * time.Millisecond
|
||||
} else {
|
||||
delay *= 2
|
||||
}
|
||||
if max := 1 * time.Second; delay > max {
|
||||
delay = max
|
||||
}
|
||||
s.logger().Printf("accept error (retrying in %v): %v", delay, err)
|
||||
time.Sleep(delay)
|
||||
continue
|
||||
} else if errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("accept error: %w", err)
|
||||
}
|
||||
|
||||
delay = 0
|
||||
go newConn(conn, s).serve()
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network address addr and then calls Serve.
|
||||
//
|
||||
// If addr is empty, ":143" is used.
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
if addr == "" {
|
||||
addr = ":143"
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(ln)
|
||||
}
|
||||
|
||||
// ListenAndServeTLS listens on the TCP network address addr and then calls
|
||||
// Serve to handle incoming TLS connections.
|
||||
//
|
||||
// The TLS configuration set in Options.TLSConfig is used. If addr is empty,
|
||||
// ":993" is used.
|
||||
func (s *Server) ListenAndServeTLS(addr string) error {
|
||||
if addr == "" {
|
||||
addr = ":993"
|
||||
}
|
||||
ln, err := tls.Listen("tcp", addr, s.options.TLSConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(ln)
|
||||
}
|
||||
|
||||
// Close immediately closes all active listeners and connections.
|
||||
//
|
||||
// Close returns any error returned from closing the server's underlying
|
||||
// listeners.
|
||||
//
|
||||
// Once Close has been called on a server, it may not be reused; future calls
|
||||
// to methods such as Serve will return an error.
|
||||
func (s *Server) Close() error {
|
||||
var err error
|
||||
|
||||
s.mutex.Lock()
|
||||
ok := !s.closed
|
||||
if ok {
|
||||
s.closed = true
|
||||
for l := range s.listeners {
|
||||
if closeErr := l.Close(); closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
if !ok {
|
||||
return errClosed
|
||||
}
|
||||
|
||||
s.listenerWaitGroup.Wait()
|
||||
|
||||
s.mutex.Lock()
|
||||
for c := range s.conns {
|
||||
c.mutex.Lock()
|
||||
c.conn.Close()
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
126
imapserver/session.go
Normal file
126
imapserver/session.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
var errAuthFailed = &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Code: imap.ResponseCodeAuthenticationFailed,
|
||||
Text: "Authentication failed",
|
||||
}
|
||||
|
||||
// ErrAuthFailed is returned by Session.Login on authentication failure.
|
||||
var ErrAuthFailed = errAuthFailed
|
||||
|
||||
// GreetingData is the data associated with an IMAP greeting.
|
||||
type GreetingData struct {
|
||||
PreAuth bool
|
||||
}
|
||||
|
||||
// NumKind describes how a number should be interpreted: either as a sequence
|
||||
// number, either as a UID.
|
||||
type NumKind int
|
||||
|
||||
const (
|
||||
NumKindSeq = NumKind(imapwire.NumKindSeq)
|
||||
NumKindUID = NumKind(imapwire.NumKindUID)
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (kind NumKind) String() string {
|
||||
switch kind {
|
||||
case NumKindSeq:
|
||||
return "seq"
|
||||
case NumKindUID:
|
||||
return "uid"
|
||||
default:
|
||||
panic(fmt.Errorf("imapserver: unknown NumKind %d", kind))
|
||||
}
|
||||
}
|
||||
|
||||
func (kind NumKind) wire() imapwire.NumKind {
|
||||
return imapwire.NumKind(kind)
|
||||
}
|
||||
|
||||
// Session is an IMAP session.
|
||||
type Session interface {
|
||||
Close() error
|
||||
|
||||
// Not authenticated state
|
||||
Login(username, password string) error
|
||||
|
||||
// Authenticated state
|
||||
Select(mailbox string, options *imap.SelectOptions) (*imap.SelectData, error)
|
||||
Create(mailbox string, options *imap.CreateOptions) error
|
||||
Delete(mailbox string) error
|
||||
Rename(mailbox, newName string, options *imap.RenameOptions) error
|
||||
Subscribe(mailbox string) error
|
||||
Unsubscribe(mailbox string) error
|
||||
List(w *ListWriter, ref string, patterns []string, options *imap.ListOptions) error
|
||||
Status(mailbox string, options *imap.StatusOptions) (*imap.StatusData, error)
|
||||
Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error)
|
||||
Poll(w *UpdateWriter, allowExpunge bool) error
|
||||
Idle(w *UpdateWriter, stop <-chan struct{}) error
|
||||
|
||||
// Selected state
|
||||
Unselect() error
|
||||
Expunge(w *ExpungeWriter, uids *imap.UIDSet) error
|
||||
Search(kind NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error)
|
||||
Fetch(w *FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error
|
||||
Store(w *FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error
|
||||
Copy(numSet imap.NumSet, dest string) (*imap.CopyData, error)
|
||||
}
|
||||
|
||||
// SessionNamespace is an IMAP session which supports NAMESPACE.
|
||||
type SessionNamespace interface {
|
||||
Session
|
||||
|
||||
// Authenticated state
|
||||
Namespace() (*imap.NamespaceData, error)
|
||||
}
|
||||
|
||||
// SessionMove is an IMAP session which supports MOVE.
|
||||
type SessionMove interface {
|
||||
Session
|
||||
|
||||
// Selected state
|
||||
Move(w *MoveWriter, numSet imap.NumSet, dest string) error
|
||||
}
|
||||
|
||||
// SessionIMAP4rev2 is an IMAP session which supports IMAP4rev2.
|
||||
type SessionIMAP4rev2 interface {
|
||||
Session
|
||||
SessionNamespace
|
||||
SessionMove
|
||||
}
|
||||
|
||||
// SessionSASL is an IMAP session which supports its own set of SASL
|
||||
// authentication mechanisms.
|
||||
type SessionSASL interface {
|
||||
Session
|
||||
AuthenticateMechanisms() []string
|
||||
Authenticate(mech string) (sasl.Server, error)
|
||||
}
|
||||
|
||||
// SessionUnauthenticate is an IMAP session which supports UNAUTHENTICATE.
|
||||
type SessionUnauthenticate interface {
|
||||
Session
|
||||
|
||||
// Authenticated state
|
||||
Unauthenticate() error
|
||||
}
|
||||
|
||||
// SessionAppendLimit is an IMAP session which has the same APPEND limit for
|
||||
// all mailboxes.
|
||||
type SessionAppendLimit interface {
|
||||
Session
|
||||
|
||||
// AppendLimit returns the maximum size in bytes that can be uploaded to
|
||||
// this server in an APPEND command.
|
||||
AppendLimit() uint32
|
||||
}
|
||||
83
imapserver/starttls.go
Normal file
83
imapserver/starttls.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) canStartTLS() bool {
|
||||
_, isTLS := c.conn.(*tls.Conn)
|
||||
return c.server.options.TLSConfig != nil && c.state == imap.ConnStateNotAuthenticated && !isTLS
|
||||
}
|
||||
|
||||
func (c *Conn) handleStartTLS(tag string, dec *imapwire.Decoder) error {
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if c.server.options.TLSConfig == nil {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeNo,
|
||||
Text: "STARTTLS not supported",
|
||||
}
|
||||
}
|
||||
if !c.canStartTLS() {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeBad,
|
||||
Text: "STARTTLS not available",
|
||||
}
|
||||
}
|
||||
|
||||
// Do not allow to write cleartext data past this point: keep c.encMutex
|
||||
// locked until the end
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
err := writeStatusResp(enc.Encoder, tag, &imap.StatusResponse{
|
||||
Type: imap.StatusResponseTypeOK,
|
||||
Text: "Begin TLS negotiation now",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drain buffered data from our bufio.Reader
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil {
|
||||
panic(err) // unreachable
|
||||
}
|
||||
|
||||
var cleartextConn net.Conn
|
||||
if buf.Len() > 0 {
|
||||
r := io.MultiReader(&buf, c.conn)
|
||||
cleartextConn = startTLSConn{c.conn, r}
|
||||
} else {
|
||||
cleartextConn = c.conn
|
||||
}
|
||||
|
||||
tlsConn := tls.Server(cleartextConn, c.server.options.TLSConfig)
|
||||
|
||||
c.mutex.Lock()
|
||||
c.conn = tlsConn
|
||||
c.mutex.Unlock()
|
||||
|
||||
rw := c.server.options.wrapReadWriter(tlsConn)
|
||||
c.br.Reset(rw)
|
||||
c.bw.Reset(rw)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type startTLSConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (conn startTLSConn) Read(b []byte) (int, error) {
|
||||
return conn.r.Read(b)
|
||||
}
|
||||
125
imapserver/status.go
Normal file
125
imapserver/status.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleStatus(dec *imapwire.Decoder) error {
|
||||
var mailbox string
|
||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&mailbox) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
var options imap.StatusOptions
|
||||
err := dec.ExpectList(func() error {
|
||||
err := readStatusItem(dec, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if options.NumRecent && !c.server.options.caps().Has(imap.CapIMAP4rev1) {
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeBad,
|
||||
Text: "Unknown STATUS data item",
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := c.session.Status(mailbox, &options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.writeStatus(data, &options)
|
||||
}
|
||||
|
||||
func (c *Conn) writeStatus(data *imap.StatusData, options *imap.StatusOptions) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom("*").SP().Atom("STATUS").SP().Mailbox(data.Mailbox).SP()
|
||||
listEnc := enc.BeginList()
|
||||
if options.NumMessages {
|
||||
listEnc.Item().Atom("MESSAGES").SP().Number(*data.NumMessages)
|
||||
}
|
||||
if options.UIDNext {
|
||||
listEnc.Item().Atom("UIDNEXT").SP().UID(data.UIDNext)
|
||||
}
|
||||
if options.UIDValidity {
|
||||
listEnc.Item().Atom("UIDVALIDITY").SP().Number(data.UIDValidity)
|
||||
}
|
||||
if options.NumUnseen {
|
||||
listEnc.Item().Atom("UNSEEN").SP().Number(*data.NumUnseen)
|
||||
}
|
||||
if options.NumDeleted {
|
||||
listEnc.Item().Atom("DELETED").SP().Number(*data.NumDeleted)
|
||||
}
|
||||
if options.Size {
|
||||
listEnc.Item().Atom("SIZE").SP().Number64(*data.Size)
|
||||
}
|
||||
if options.AppendLimit {
|
||||
listEnc.Item().Atom("APPENDLIMIT").SP()
|
||||
if data.AppendLimit != nil {
|
||||
enc.Number(*data.AppendLimit)
|
||||
} else {
|
||||
enc.NIL()
|
||||
}
|
||||
}
|
||||
if options.DeletedStorage {
|
||||
listEnc.Item().Atom("DELETED-STORAGE").SP().Number64(*data.DeletedStorage)
|
||||
}
|
||||
if options.NumRecent {
|
||||
listEnc.Item().Atom("RECENT").SP().Number(*data.NumRecent)
|
||||
}
|
||||
listEnc.End()
|
||||
|
||||
return enc.CRLF()
|
||||
}
|
||||
|
||||
func readStatusItem(dec *imapwire.Decoder, options *imap.StatusOptions) error {
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return dec.Err()
|
||||
}
|
||||
switch strings.ToUpper(name) {
|
||||
case "MESSAGES":
|
||||
options.NumMessages = true
|
||||
case "UIDNEXT":
|
||||
options.UIDNext = true
|
||||
case "UIDVALIDITY":
|
||||
options.UIDValidity = true
|
||||
case "UNSEEN":
|
||||
options.NumUnseen = true
|
||||
case "DELETED":
|
||||
options.NumDeleted = true
|
||||
case "SIZE":
|
||||
options.Size = true
|
||||
case "APPENDLIMIT":
|
||||
options.AppendLimit = true
|
||||
case "DELETED-STORAGE":
|
||||
options.DeletedStorage = true
|
||||
case "RECENT":
|
||||
options.NumRecent = true
|
||||
default:
|
||||
return &imap.Error{
|
||||
Type: imap.StatusResponseTypeBad,
|
||||
Text: "Unknown STATUS data item",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
78
imapserver/store.go
Normal file
78
imapserver/store.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleStore(dec *imapwire.Decoder, numKind NumKind) error {
|
||||
var (
|
||||
numSet imap.NumSet
|
||||
item string
|
||||
)
|
||||
if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() || !dec.ExpectAtom(&item) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
var flags []imap.Flag
|
||||
isList, err := dec.List(func() error {
|
||||
flag, err := internal.ExpectFlag(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flags = append(flags, flag)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !isList {
|
||||
for {
|
||||
flag, err := internal.ExpectFlag(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flags = append(flags, flag)
|
||||
|
||||
if !dec.SP() {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !dec.ExpectCRLF() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
item = strings.ToUpper(item)
|
||||
silent := strings.HasSuffix(item, ".SILENT")
|
||||
item = strings.TrimSuffix(item, ".SILENT")
|
||||
|
||||
var op imap.StoreFlagsOp
|
||||
switch {
|
||||
case strings.HasPrefix(item, "+"):
|
||||
op = imap.StoreFlagsAdd
|
||||
item = strings.TrimPrefix(item, "+")
|
||||
case strings.HasPrefix(item, "-"):
|
||||
op = imap.StoreFlagsDel
|
||||
item = strings.TrimPrefix(item, "-")
|
||||
default:
|
||||
op = imap.StoreFlagsSet
|
||||
}
|
||||
|
||||
if item != "FLAGS" {
|
||||
return newClientBugError("STORE can only change FLAGS")
|
||||
}
|
||||
|
||||
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := &FetchWriter{conn: c}
|
||||
options := imap.StoreOptions{}
|
||||
return c.session.Store(w, numSet, &imap.StoreFlags{
|
||||
Op: op,
|
||||
Silent: silent,
|
||||
Flags: flags,
|
||||
}, &options)
|
||||
}
|
||||
284
imapserver/tracker.go
Normal file
284
imapserver/tracker.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// MailboxTracker tracks the state of a mailbox.
|
||||
//
|
||||
// A mailbox can have multiple sessions listening for updates. Each session has
|
||||
// its own view of the mailbox, because IMAP clients asynchronously receive
|
||||
// mailbox updates.
|
||||
type MailboxTracker struct {
|
||||
mutex sync.Mutex
|
||||
numMessages uint32
|
||||
sessions map[*SessionTracker]struct{}
|
||||
}
|
||||
|
||||
// NewMailboxTracker creates a new mailbox tracker.
|
||||
func NewMailboxTracker(numMessages uint32) *MailboxTracker {
|
||||
return &MailboxTracker{
|
||||
numMessages: numMessages,
|
||||
sessions: make(map[*SessionTracker]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// NewSession creates a new session tracker for the mailbox.
|
||||
//
|
||||
// The caller must call SessionTracker.Close once they are done with the
|
||||
// session.
|
||||
func (t *MailboxTracker) NewSession() *SessionTracker {
|
||||
st := &SessionTracker{mailbox: t}
|
||||
t.mutex.Lock()
|
||||
t.sessions[st] = struct{}{}
|
||||
t.mutex.Unlock()
|
||||
return st
|
||||
}
|
||||
|
||||
func (t *MailboxTracker) queueUpdate(update *trackerUpdate, source *SessionTracker) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
if update.expunge != 0 && update.expunge > t.numMessages {
|
||||
panic(fmt.Errorf("imapserver: expunge sequence number (%v) out of range (%v messages in mailbox)", update.expunge, t.numMessages))
|
||||
}
|
||||
if update.numMessages != 0 && update.numMessages < t.numMessages {
|
||||
panic(fmt.Errorf("imapserver: cannot decrease mailbox number of messages from %v to %v", t.numMessages, update.numMessages))
|
||||
}
|
||||
|
||||
for st := range t.sessions {
|
||||
if source != nil && st == source {
|
||||
continue
|
||||
}
|
||||
st.queueUpdate(update)
|
||||
}
|
||||
|
||||
switch {
|
||||
case update.expunge != 0:
|
||||
t.numMessages--
|
||||
case update.numMessages != 0:
|
||||
t.numMessages = update.numMessages
|
||||
}
|
||||
}
|
||||
|
||||
// QueueExpunge queues a new EXPUNGE update.
|
||||
func (t *MailboxTracker) QueueExpunge(seqNum uint32) {
|
||||
if seqNum == 0 {
|
||||
panic("imapserver: invalid expunge message sequence number")
|
||||
}
|
||||
t.queueUpdate(&trackerUpdate{expunge: seqNum}, nil)
|
||||
}
|
||||
|
||||
// QueueNumMessages queues a new EXISTS update.
|
||||
func (t *MailboxTracker) QueueNumMessages(n uint32) {
|
||||
// TODO: merge consecutive NumMessages updates
|
||||
t.queueUpdate(&trackerUpdate{numMessages: n}, nil)
|
||||
}
|
||||
|
||||
// QueueMailboxFlags queues a new FLAGS update.
|
||||
func (t *MailboxTracker) QueueMailboxFlags(flags []imap.Flag) {
|
||||
if flags == nil {
|
||||
flags = []imap.Flag{}
|
||||
}
|
||||
t.queueUpdate(&trackerUpdate{mailboxFlags: flags}, nil)
|
||||
}
|
||||
|
||||
// QueueMessageFlags queues a new FETCH FLAGS update.
|
||||
//
|
||||
// If source is not nil, the update won't be dispatched to it.
|
||||
func (t *MailboxTracker) QueueMessageFlags(seqNum uint32, uid imap.UID, flags []imap.Flag, source *SessionTracker) {
|
||||
t.queueUpdate(&trackerUpdate{fetch: &trackerUpdateFetch{
|
||||
seqNum: seqNum,
|
||||
uid: uid,
|
||||
flags: flags,
|
||||
}}, source)
|
||||
}
|
||||
|
||||
type trackerUpdate struct {
|
||||
expunge uint32
|
||||
numMessages uint32
|
||||
mailboxFlags []imap.Flag
|
||||
fetch *trackerUpdateFetch
|
||||
}
|
||||
|
||||
type trackerUpdateFetch struct {
|
||||
seqNum uint32
|
||||
uid imap.UID
|
||||
flags []imap.Flag
|
||||
}
|
||||
|
||||
// SessionTracker tracks the state of a mailbox for an IMAP client.
|
||||
type SessionTracker struct {
|
||||
mailbox *MailboxTracker
|
||||
|
||||
mutex sync.Mutex
|
||||
queue []trackerUpdate
|
||||
updates chan<- struct{}
|
||||
}
|
||||
|
||||
// Close unregisters the session.
|
||||
func (t *SessionTracker) Close() {
|
||||
t.mailbox.mutex.Lock()
|
||||
delete(t.mailbox.sessions, t)
|
||||
t.mailbox.mutex.Unlock()
|
||||
t.mailbox = nil
|
||||
}
|
||||
|
||||
func (t *SessionTracker) queueUpdate(update *trackerUpdate) {
|
||||
var updates chan<- struct{}
|
||||
t.mutex.Lock()
|
||||
t.queue = append(t.queue, *update)
|
||||
updates = t.updates
|
||||
t.mutex.Unlock()
|
||||
|
||||
if updates != nil {
|
||||
select {
|
||||
case updates <- struct{}{}:
|
||||
// we notified SessionTracker.Idle about the update
|
||||
default:
|
||||
// skip the update
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll dequeues pending mailbox updates for this session.
|
||||
func (t *SessionTracker) Poll(w *UpdateWriter, allowExpunge bool) error {
|
||||
var updates []trackerUpdate
|
||||
t.mutex.Lock()
|
||||
if allowExpunge {
|
||||
updates = t.queue
|
||||
t.queue = nil
|
||||
} else {
|
||||
stopIndex := -1
|
||||
for i, update := range t.queue {
|
||||
if update.expunge != 0 {
|
||||
stopIndex = i
|
||||
break
|
||||
}
|
||||
updates = append(updates, update)
|
||||
}
|
||||
if stopIndex >= 0 {
|
||||
t.queue = t.queue[stopIndex:]
|
||||
} else {
|
||||
t.queue = nil
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
|
||||
for _, update := range updates {
|
||||
var err error
|
||||
switch {
|
||||
case update.expunge != 0:
|
||||
err = w.WriteExpunge(update.expunge)
|
||||
case update.numMessages != 0:
|
||||
err = w.WriteNumMessages(update.numMessages)
|
||||
case update.mailboxFlags != nil:
|
||||
err = w.WriteMailboxFlags(update.mailboxFlags)
|
||||
case update.fetch != nil:
|
||||
err = w.WriteMessageFlags(update.fetch.seqNum, update.fetch.uid, update.fetch.flags)
|
||||
default:
|
||||
panic(fmt.Errorf("imapserver: unknown tracker update %#v", update))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Idle continuously writes mailbox updates.
|
||||
//
|
||||
// When the stop channel is closed, it returns.
|
||||
//
|
||||
// Idle cannot be invoked concurrently from two separate goroutines.
|
||||
func (t *SessionTracker) Idle(w *UpdateWriter, stop <-chan struct{}) error {
|
||||
updates := make(chan struct{}, 64)
|
||||
t.mutex.Lock()
|
||||
ok := t.updates == nil
|
||||
if ok {
|
||||
t.updates = updates
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("imapserver: only a single SessionTracker.Idle call is allowed at a time")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.mutex.Lock()
|
||||
t.updates = nil
|
||||
t.mutex.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-updates:
|
||||
if err := t.Poll(w, true); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-stop:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeSeqNum converts a message sequence number from the client view to the
|
||||
// server view.
|
||||
//
|
||||
// Zero is returned if the message doesn't exist from the server point-of-view.
|
||||
func (t *SessionTracker) DecodeSeqNum(seqNum uint32) uint32 {
|
||||
if seqNum == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
for _, update := range t.queue {
|
||||
if update.expunge == 0 {
|
||||
continue
|
||||
}
|
||||
if seqNum == update.expunge {
|
||||
return 0
|
||||
} else if seqNum > update.expunge {
|
||||
seqNum--
|
||||
}
|
||||
}
|
||||
|
||||
if seqNum > t.mailbox.numMessages {
|
||||
return 0
|
||||
}
|
||||
|
||||
return seqNum
|
||||
}
|
||||
|
||||
// EncodeSeqNum converts a message sequence number from the server view to the
|
||||
// client view.
|
||||
//
|
||||
// Zero is returned if the message doesn't exist from the client point-of-view.
|
||||
func (t *SessionTracker) EncodeSeqNum(seqNum uint32) uint32 {
|
||||
if seqNum == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
if seqNum > t.mailbox.numMessages {
|
||||
return 0
|
||||
}
|
||||
|
||||
for i := len(t.queue) - 1; i >= 0; i-- {
|
||||
update := t.queue[i]
|
||||
// TODO: this doesn't handle increments > 1
|
||||
if update.numMessages != 0 && seqNum == update.numMessages {
|
||||
return 0
|
||||
}
|
||||
if update.expunge != 0 && seqNum >= update.expunge {
|
||||
seqNum++
|
||||
}
|
||||
}
|
||||
return seqNum
|
||||
}
|
||||
155
imapserver/tracker_test.go
Normal file
155
imapserver/tracker_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package imapserver_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
type trackerUpdate struct {
|
||||
expunge uint32
|
||||
numMessages uint32
|
||||
}
|
||||
|
||||
var sessionTrackerSeqNumTests = []struct {
|
||||
name string
|
||||
pending []trackerUpdate
|
||||
clientSeqNum, serverSeqNum uint32
|
||||
}{
|
||||
{
|
||||
name: "noop",
|
||||
pending: nil,
|
||||
clientSeqNum: 20,
|
||||
serverSeqNum: 20,
|
||||
},
|
||||
{
|
||||
name: "noop_last",
|
||||
pending: nil,
|
||||
clientSeqNum: 42,
|
||||
serverSeqNum: 42,
|
||||
},
|
||||
{
|
||||
name: "noop_client_oob",
|
||||
pending: nil,
|
||||
clientSeqNum: 43,
|
||||
serverSeqNum: 0,
|
||||
},
|
||||
{
|
||||
name: "noop_server_oob",
|
||||
pending: nil,
|
||||
clientSeqNum: 0,
|
||||
serverSeqNum: 43,
|
||||
},
|
||||
{
|
||||
name: "expunge_eq",
|
||||
pending: []trackerUpdate{{expunge: 20}},
|
||||
clientSeqNum: 20,
|
||||
serverSeqNum: 0,
|
||||
},
|
||||
{
|
||||
name: "expunge_lt",
|
||||
pending: []trackerUpdate{{expunge: 20}},
|
||||
clientSeqNum: 10,
|
||||
serverSeqNum: 10,
|
||||
},
|
||||
{
|
||||
name: "expunge_gt",
|
||||
pending: []trackerUpdate{{expunge: 10}},
|
||||
clientSeqNum: 20,
|
||||
serverSeqNum: 19,
|
||||
},
|
||||
{
|
||||
name: "append_eq",
|
||||
pending: []trackerUpdate{{numMessages: 43}},
|
||||
clientSeqNum: 0,
|
||||
serverSeqNum: 43,
|
||||
},
|
||||
{
|
||||
name: "append_lt",
|
||||
pending: []trackerUpdate{{numMessages: 43}},
|
||||
clientSeqNum: 42,
|
||||
serverSeqNum: 42,
|
||||
},
|
||||
{
|
||||
name: "expunge_append",
|
||||
pending: []trackerUpdate{
|
||||
{expunge: 42},
|
||||
{numMessages: 42},
|
||||
},
|
||||
clientSeqNum: 42,
|
||||
serverSeqNum: 0,
|
||||
},
|
||||
{
|
||||
name: "expunge_append",
|
||||
pending: []trackerUpdate{
|
||||
{expunge: 42},
|
||||
{numMessages: 42},
|
||||
},
|
||||
clientSeqNum: 0,
|
||||
serverSeqNum: 42,
|
||||
},
|
||||
{
|
||||
name: "append_expunge",
|
||||
pending: []trackerUpdate{
|
||||
{numMessages: 43},
|
||||
{expunge: 42},
|
||||
},
|
||||
clientSeqNum: 42,
|
||||
serverSeqNum: 0,
|
||||
},
|
||||
{
|
||||
name: "append_expunge",
|
||||
pending: []trackerUpdate{
|
||||
{numMessages: 43},
|
||||
{expunge: 42},
|
||||
},
|
||||
clientSeqNum: 0,
|
||||
serverSeqNum: 42,
|
||||
},
|
||||
{
|
||||
name: "multi_expunge_middle",
|
||||
pending: []trackerUpdate{
|
||||
{expunge: 3},
|
||||
{expunge: 1},
|
||||
},
|
||||
clientSeqNum: 2,
|
||||
serverSeqNum: 1,
|
||||
},
|
||||
{
|
||||
name: "multi_expunge_after",
|
||||
pending: []trackerUpdate{
|
||||
{expunge: 3},
|
||||
{expunge: 1},
|
||||
},
|
||||
clientSeqNum: 4,
|
||||
serverSeqNum: 2,
|
||||
},
|
||||
}
|
||||
|
||||
func TestSessionTracker(t *testing.T) {
|
||||
for _, tc := range sessionTrackerSeqNumTests {
|
||||
tc := tc // capture range variable
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mboxTracker := imapserver.NewMailboxTracker(42)
|
||||
sessTracker := mboxTracker.NewSession()
|
||||
for _, update := range tc.pending {
|
||||
switch {
|
||||
case update.expunge != 0:
|
||||
mboxTracker.QueueExpunge(update.expunge)
|
||||
case update.numMessages != 0:
|
||||
mboxTracker.QueueNumMessages(update.numMessages)
|
||||
}
|
||||
}
|
||||
|
||||
serverSeqNum := sessTracker.DecodeSeqNum(tc.clientSeqNum)
|
||||
if tc.clientSeqNum != 0 && serverSeqNum != tc.serverSeqNum {
|
||||
t.Errorf("DecodeSeqNum(%v): got %v, want %v", tc.clientSeqNum, serverSeqNum, tc.serverSeqNum)
|
||||
}
|
||||
|
||||
clientSeqNum := sessTracker.EncodeSeqNum(tc.serverSeqNum)
|
||||
if tc.serverSeqNum != 0 && clientSeqNum != tc.clientSeqNum {
|
||||
t.Errorf("EncodeSeqNum(%v): got %v, want %v", tc.serverSeqNum, clientSeqNum, tc.clientSeqNum)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user