This is the v1 version, had the v2 before.
This commit is contained in:
52
server/cmd_any.go
Normal file
52
server/cmd_any.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/backend"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
)
|
||||
|
||||
type Capability struct {
|
||||
commands.Capability
|
||||
}
|
||||
|
||||
func (cmd *Capability) Handle(conn Conn) error {
|
||||
res := &responses.Capability{Caps: conn.Capabilities()}
|
||||
return conn.WriteResp(res)
|
||||
}
|
||||
|
||||
type Noop struct {
|
||||
commands.Noop
|
||||
}
|
||||
|
||||
func (cmd *Noop) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox != nil {
|
||||
// If a mailbox is selected, NOOP can be used to poll for server updates
|
||||
if mbox, ok := ctx.Mailbox.(backend.MailboxPoller); ok {
|
||||
return mbox.Poll()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Logout struct {
|
||||
commands.Logout
|
||||
}
|
||||
|
||||
func (cmd *Logout) Handle(conn Conn) error {
|
||||
res := &imap.StatusResp{
|
||||
Type: imap.StatusRespBye,
|
||||
Info: "Closing connection",
|
||||
}
|
||||
|
||||
if err := conn.WriteResp(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Request to close the connection
|
||||
conn.Context().State = imap.LogoutState
|
||||
return nil
|
||||
}
|
||||
129
server/cmd_any_test.go
Normal file
129
server/cmd_any_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/server"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
func testServerGreeted(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) {
|
||||
s, c = testServer(t)
|
||||
scanner = bufio.NewScanner(c)
|
||||
|
||||
scanner.Scan() // Greeting
|
||||
return
|
||||
}
|
||||
|
||||
func TestCapability(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CAPABILITY\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN" {
|
||||
t.Fatal("Bad capability:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 NOOP\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LOGOUT\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "* BYE ") {
|
||||
t.Fatal("Bad BYE response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
type xnoop struct{}
|
||||
|
||||
func (ext *xnoop) Capabilities(server.Conn) []string {
|
||||
return []string{"XNOOP"}
|
||||
}
|
||||
|
||||
func (ext *xnoop) Command(string) server.HandlerFactory {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestServer_Enable(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
s.Enable(&xnoop{})
|
||||
|
||||
io.WriteString(c, "a001 CAPABILITY\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN XNOOP" {
|
||||
t.Fatal("Bad capability:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
type xnoopAuth struct{}
|
||||
|
||||
func (ext *xnoopAuth) Next(response []byte) (challenge []byte, done bool, err error) {
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
func TestServer_EnableAuth(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
s.EnableAuth("XNOOP", func(server.Conn) sasl.Server {
|
||||
return &xnoopAuth{}
|
||||
})
|
||||
|
||||
io.WriteString(c, "a001 CAPABILITY\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN AUTH=XNOOP" &&
|
||||
scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=XNOOP AUTH=PLAIN" {
|
||||
t.Fatal("Bad capability:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
324
server/cmd_auth.go
Normal file
324
server/cmd_auth.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/backend"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
)
|
||||
|
||||
// imap errors in Authenticated state.
|
||||
var (
|
||||
ErrNotAuthenticated = errors.New("Not authenticated")
|
||||
)
|
||||
|
||||
type Select struct {
|
||||
commands.Select
|
||||
}
|
||||
|
||||
func (cmd *Select) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
|
||||
// As per RFC1730#6.3.1,
|
||||
// The SELECT command automatically deselects any
|
||||
// currently selected mailbox before attempting the new selection.
|
||||
// Consequently, if a mailbox is selected and a SELECT command that
|
||||
// fails is attempted, no mailbox is selected.
|
||||
// For example, some clients (e.g. Apple Mail) perform SELECT "" when the
|
||||
// server doesn't announce the UNSELECT capability.
|
||||
ctx.Mailbox = nil
|
||||
ctx.MailboxReadOnly = false
|
||||
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items := []imap.StatusItem{
|
||||
imap.StatusMessages, imap.StatusRecent, imap.StatusUnseen,
|
||||
imap.StatusUidNext, imap.StatusUidValidity,
|
||||
}
|
||||
|
||||
status, err := mbox.Status(items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Mailbox = mbox
|
||||
ctx.MailboxReadOnly = cmd.ReadOnly || status.ReadOnly
|
||||
|
||||
res := &responses.Select{Mailbox: status}
|
||||
if err := conn.WriteResp(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var code imap.StatusRespCode = imap.CodeReadWrite
|
||||
if ctx.MailboxReadOnly {
|
||||
code = imap.CodeReadOnly
|
||||
}
|
||||
return ErrStatusResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Code: code,
|
||||
})
|
||||
}
|
||||
|
||||
type Create struct {
|
||||
commands.Create
|
||||
}
|
||||
|
||||
func (cmd *Create) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
return ctx.User.CreateMailbox(cmd.Mailbox)
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
commands.Delete
|
||||
}
|
||||
|
||||
func (cmd *Delete) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
return ctx.User.DeleteMailbox(cmd.Mailbox)
|
||||
}
|
||||
|
||||
type Rename struct {
|
||||
commands.Rename
|
||||
}
|
||||
|
||||
func (cmd *Rename) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
return ctx.User.RenameMailbox(cmd.Existing, cmd.New)
|
||||
}
|
||||
|
||||
type Subscribe struct {
|
||||
commands.Subscribe
|
||||
}
|
||||
|
||||
func (cmd *Subscribe) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mbox.SetSubscribed(true)
|
||||
}
|
||||
|
||||
type Unsubscribe struct {
|
||||
commands.Unsubscribe
|
||||
}
|
||||
|
||||
func (cmd *Unsubscribe) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mbox.SetSubscribed(false)
|
||||
}
|
||||
|
||||
type List struct {
|
||||
commands.List
|
||||
}
|
||||
|
||||
func (cmd *List) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
ch := make(chan *imap.MailboxInfo)
|
||||
res := &responses.List{Mailboxes: ch, Subscribed: cmd.Subscribed}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go (func() {
|
||||
done <- conn.WriteResp(res)
|
||||
// Make sure to drain the channel.
|
||||
for range ch {
|
||||
}
|
||||
})()
|
||||
|
||||
mailboxes, err := ctx.User.ListMailboxes(cmd.Subscribed)
|
||||
if err != nil {
|
||||
// Close channel to signal end of results
|
||||
close(ch)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, mbox := range mailboxes {
|
||||
info, err := mbox.Info()
|
||||
if err != nil {
|
||||
// Close channel to signal end of results
|
||||
close(ch)
|
||||
return err
|
||||
}
|
||||
|
||||
// An empty ("" string) mailbox name argument is a special request to return
|
||||
// the hierarchy delimiter and the root name of the name given in the
|
||||
// reference.
|
||||
if cmd.Mailbox == "" {
|
||||
ch <- &imap.MailboxInfo{
|
||||
Attributes: []string{imap.NoSelectAttr},
|
||||
Delimiter: info.Delimiter,
|
||||
Name: info.Delimiter,
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if info.Match(cmd.Reference, cmd.Mailbox) {
|
||||
ch <- info
|
||||
}
|
||||
}
|
||||
// Close channel to signal end of results
|
||||
close(ch)
|
||||
|
||||
return <-done
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
commands.Status
|
||||
}
|
||||
|
||||
func (cmd *Status) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := mbox.Status(cmd.Items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only keep items thqat have been requested
|
||||
items := make(map[imap.StatusItem]interface{})
|
||||
for _, k := range cmd.Items {
|
||||
items[k] = status.Items[k]
|
||||
}
|
||||
status.Items = items
|
||||
|
||||
res := &responses.Status{Mailbox: status}
|
||||
return conn.WriteResp(res)
|
||||
}
|
||||
|
||||
type Append struct {
|
||||
commands.Append
|
||||
}
|
||||
|
||||
func (cmd *Append) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.User == nil {
|
||||
return ErrNotAuthenticated
|
||||
}
|
||||
|
||||
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
|
||||
if err == backend.ErrNoSuchMailbox {
|
||||
return ErrStatusResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespNo,
|
||||
Code: imap.CodeTryCreate,
|
||||
Info: err.Error(),
|
||||
})
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := mbox.CreateMessage(cmd.Flags, cmd.Date, cmd.Message); err != nil {
|
||||
if err == backend.ErrTooBig {
|
||||
return ErrStatusResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespNo,
|
||||
Code: "TOOBIG",
|
||||
Info: "Message size exceeding limit",
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// If APPEND targets the currently selected mailbox, send an untagged EXISTS
|
||||
// Do this only if the backend doesn't send updates itself
|
||||
if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == mbox.Name() {
|
||||
status, err := mbox.Status([]imap.StatusItem{imap.StatusMessages})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status.Flags = nil
|
||||
status.PermanentFlags = nil
|
||||
status.UnseenSeqNum = 0
|
||||
|
||||
res := &responses.Select{Mailbox: status}
|
||||
if err := conn.WriteResp(res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Unselect struct {
|
||||
commands.Unselect
|
||||
}
|
||||
|
||||
func (cmd *Unselect) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
|
||||
ctx.Mailbox = nil
|
||||
ctx.MailboxReadOnly = false
|
||||
return nil
|
||||
}
|
||||
|
||||
type Idle struct {
|
||||
commands.Idle
|
||||
}
|
||||
|
||||
func (cmd *Idle) Handle(conn Conn) error {
|
||||
cont := &imap.ContinuationReq{Info: "idling"}
|
||||
if err := conn.WriteResp(cont); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for DONE
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Scan()
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToUpper(scanner.Text()) != "DONE" {
|
||||
return errors.New("Expected DONE")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
611
server/cmd_auth_test.go
Normal file
611
server/cmd_auth_test.go
Normal file
@@ -0,0 +1,611 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/server"
|
||||
)
|
||||
|
||||
func testServerAuthenticated(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) {
|
||||
s, c, scanner = testServerGreeted(t)
|
||||
|
||||
io.WriteString(c, "a000 LOGIN username password\r\n")
|
||||
scanner.Scan() // OK response
|
||||
return
|
||||
}
|
||||
|
||||
func TestSelect_Ok(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SELECT INBOX\r\n")
|
||||
|
||||
got := map[string]bool{
|
||||
"OK": false,
|
||||
"FLAGS": false,
|
||||
"EXISTS": false,
|
||||
"RECENT": false,
|
||||
"PERMANENTFLAGS": false,
|
||||
"UIDNEXT": false,
|
||||
"UIDVALIDITY": false,
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
res := scanner.Text()
|
||||
|
||||
if res == "* FLAGS (\\Seen)" {
|
||||
got["FLAGS"] = true
|
||||
} else if res == "* 1 EXISTS" {
|
||||
got["EXISTS"] = true
|
||||
} else if res == "* 0 RECENT" {
|
||||
got["RECENT"] = true
|
||||
} else if strings.HasPrefix(res, "* OK [PERMANENTFLAGS (\\*)]") {
|
||||
got["PERMANENTFLAGS"] = true
|
||||
} else if strings.HasPrefix(res, "* OK [UIDNEXT 7]") {
|
||||
got["UIDNEXT"] = true
|
||||
} else if strings.HasPrefix(res, "* OK [UIDVALIDITY 1]") {
|
||||
got["UIDVALIDITY"] = true
|
||||
} else if strings.HasPrefix(res, "a001 OK [READ-WRITE] ") {
|
||||
got["OK"] = true
|
||||
break
|
||||
} else {
|
||||
t.Fatal("Unexpected response:", res)
|
||||
}
|
||||
}
|
||||
|
||||
for name, val := range got {
|
||||
if !val {
|
||||
t.Error("Did not got response:", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_ReadOnly(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 EXAMINE INBOX\r\n")
|
||||
|
||||
gotOk := true
|
||||
for scanner.Scan() {
|
||||
res := scanner.Text()
|
||||
|
||||
if strings.HasPrefix(res, "a001 OK [READ-ONLY]") {
|
||||
gotOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !gotOk {
|
||||
t.Error("Did not get a correct OK response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_InvalidMailbox(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SELECT idontexist\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SELECT INBOX\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE test\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE test\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE test\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
io.WriteString(c, "a001 DELETE test\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete_InvalidMailbox(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 DELETE test\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 DELETE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRename(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE test\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
io.WriteString(c, "a001 RENAME test test2\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRename_InvalidMailbox(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 RENAME test test2\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRename_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 RENAME test test2\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribe(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 SUBSCRIBE idontexist\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribe_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
io.WriteString(c, "a001 UNSUBSCRIBE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 UNSUBSCRIBE idontexist\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribe_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 UNSUBSCRIBE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LIST \"\" *\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* LIST () \"/\" INBOX" {
|
||||
t.Fatal("Invalid LIST response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestList_Nested(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE first\r\n")
|
||||
scanner.Scan()
|
||||
io.WriteString(c, "a001 CREATE first/second\r\n")
|
||||
scanner.Scan()
|
||||
io.WriteString(c, "a001 CREATE first/second/third\r\n")
|
||||
scanner.Scan()
|
||||
io.WriteString(c, "a001 CREATE first/second/third2\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
check := func(mailboxes []string) {
|
||||
checked := map[string]bool{}
|
||||
|
||||
for scanner.Scan() {
|
||||
if strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
break
|
||||
} else if strings.HasPrefix(scanner.Text(), "* LIST ") {
|
||||
found := false
|
||||
for _, name := range mailboxes {
|
||||
if strings.HasSuffix(scanner.Text(), " \""+name+"\"") || strings.HasSuffix(scanner.Text(), " "+name) {
|
||||
checked[name] = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("Unexpected mailbox:", scanner.Text())
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Invalid LIST response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range mailboxes {
|
||||
if !checked[name] {
|
||||
t.Fatal("Missing mailbox:", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 LIST \"\" *\r\n")
|
||||
check([]string{"INBOX", "first", "first/second", "first/second/third", "first/second/third2"})
|
||||
|
||||
io.WriteString(c, "a001 LIST \"\" %\r\n")
|
||||
check([]string{"INBOX", "first"})
|
||||
|
||||
io.WriteString(c, "a001 LIST first *\r\n")
|
||||
check([]string{"first/second", "first/second/third", "first/second/third2"})
|
||||
|
||||
io.WriteString(c, "a001 LIST first %\r\n")
|
||||
check([]string{"first/second"})
|
||||
|
||||
io.WriteString(c, "a001 LIST first/second *\r\n")
|
||||
check([]string{"first/second/third", "first/second/third2"})
|
||||
|
||||
io.WriteString(c, "a001 LIST first/second %\r\n")
|
||||
check([]string{"first/second/third", "first/second/third2"})
|
||||
|
||||
io.WriteString(c, "a001 LIST first second\r\n")
|
||||
check([]string{"first/second"})
|
||||
|
||||
io.WriteString(c, "a001 LIST first/second third\r\n")
|
||||
check([]string{"first/second/third"})
|
||||
}
|
||||
|
||||
func TestList_Subscribed(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LSUB \"\" *\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 SUBSCRIBE INBOX\r\n")
|
||||
scanner.Scan()
|
||||
|
||||
io.WriteString(c, "a001 LSUB \"\" *\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* LSUB () \"/\" INBOX" {
|
||||
t.Fatal("Invalid LSUB response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLS_AlreadyAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STARTTLS\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestList_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LIST \"\" *\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestList_Delimiter(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LIST \"\" \"\"\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* LIST (\\Noselect) \"/\" \"/\"" {
|
||||
t.Fatal("Invalid LIST response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STATUS INBOX (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "* STATUS INBOX (") {
|
||||
t.Fatal("Invalid STATUS response:", line)
|
||||
}
|
||||
parts := []string{"MESSAGES 1", "RECENT 0", "UIDNEXT 7", "UIDVALIDITY 1", "UNSEEN 0"}
|
||||
for _, p := range parts {
|
||||
if !strings.Contains(line, p) {
|
||||
t.Fatal("Invalid STATUS response:", line)
|
||||
}
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus_InvalidMailbox(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STATUS idontexist (MESSAGES)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STATUS INBOX (MESSAGES)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 APPEND INBOX {80}\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "+ ") {
|
||||
t.Fatal("Invalid continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "From: Edward Snowden <root@nsa.gov>\r\n")
|
||||
io.WriteString(c, "To: Julian Assange <root@gchq.gov.uk>\r\n")
|
||||
io.WriteString(c, "\r\n")
|
||||
io.WriteString(c, "<3\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend_WithFlags(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 APPEND INBOX (\\Draft) {11}\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "+ ") {
|
||||
t.Fatal("Invalid continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "Hello World\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend_WithFlagsAndDate(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 APPEND INBOX (\\Draft) \"5-Nov-1984 13:37:00 -0700\" {11}\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "+ ") {
|
||||
t.Fatal("Invalid continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "Hello World\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend_Selected(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 APPEND INBOX {11}\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "+ ") {
|
||||
t.Fatal("Invalid continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "Hello World\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 2 EXISTS" {
|
||||
t.Fatal("Invalid untagged response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend_InvalidMailbox(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 APPEND idontexist {11}\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "+ ") {
|
||||
t.Fatal("Invalid continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "Hello World\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend_NotAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 APPEND INBOX {11}\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "+ ") {
|
||||
t.Fatal("Invalid continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "Hello World\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
132
server/cmd_noauth.go
Normal file
132
server/cmd_noauth.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
// IMAP errors in Not Authenticated state.
|
||||
var (
|
||||
ErrAlreadyAuthenticated = errors.New("Already authenticated")
|
||||
ErrAuthDisabled = errors.New("Authentication disabled")
|
||||
)
|
||||
|
||||
type StartTLS struct {
|
||||
commands.StartTLS
|
||||
}
|
||||
|
||||
func (cmd *StartTLS) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.State != imap.NotAuthenticatedState {
|
||||
return ErrAlreadyAuthenticated
|
||||
}
|
||||
if conn.IsTLS() {
|
||||
return errors.New("TLS is already enabled")
|
||||
}
|
||||
if conn.Server().TLSConfig == nil {
|
||||
return errors.New("TLS support not enabled")
|
||||
}
|
||||
|
||||
// Send an OK status response to let the client know that the TLS handshake
|
||||
// can begin
|
||||
return ErrStatusResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Info: "Begin TLS negotiation now",
|
||||
})
|
||||
}
|
||||
|
||||
func (cmd *StartTLS) Upgrade(conn Conn) error {
|
||||
tlsConfig := conn.Server().TLSConfig
|
||||
|
||||
var tlsConn *tls.Conn
|
||||
err := conn.Upgrade(func(sock net.Conn) (net.Conn, error) {
|
||||
conn.WaitReady()
|
||||
tlsConn = tls.Server(sock, tlsConfig)
|
||||
err := tlsConn.Handshake()
|
||||
return tlsConn, err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.setTLSConn(tlsConn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func afterAuthStatus(conn Conn) error {
|
||||
caps := conn.Capabilities()
|
||||
capAtoms := make([]interface{}, 0, len(caps))
|
||||
for _, cap := range caps {
|
||||
capAtoms = append(capAtoms, imap.RawString(cap))
|
||||
}
|
||||
|
||||
return ErrStatusResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Code: imap.CodeCapability,
|
||||
Arguments: capAtoms,
|
||||
})
|
||||
}
|
||||
|
||||
func canAuth(conn Conn) bool {
|
||||
for _, cap := range conn.Capabilities() {
|
||||
if cap == "AUTH=PLAIN" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
commands.Login
|
||||
}
|
||||
|
||||
func (cmd *Login) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.State != imap.NotAuthenticatedState {
|
||||
return ErrAlreadyAuthenticated
|
||||
}
|
||||
if !canAuth(conn) {
|
||||
return ErrAuthDisabled
|
||||
}
|
||||
|
||||
user, err := conn.Server().Backend.Login(conn.Info(), cmd.Username, cmd.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.State = imap.AuthenticatedState
|
||||
ctx.User = user
|
||||
return afterAuthStatus(conn)
|
||||
}
|
||||
|
||||
type Authenticate struct {
|
||||
commands.Authenticate
|
||||
}
|
||||
|
||||
func (cmd *Authenticate) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.State != imap.NotAuthenticatedState {
|
||||
return ErrAlreadyAuthenticated
|
||||
}
|
||||
if !canAuth(conn) {
|
||||
return ErrAuthDisabled
|
||||
}
|
||||
|
||||
mechanisms := map[string]sasl.Server{}
|
||||
for name, newSasl := range conn.Server().auths {
|
||||
mechanisms[name] = newSasl(conn)
|
||||
}
|
||||
|
||||
err := cmd.Authenticate.Handle(mechanisms, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return afterAuthStatus(conn)
|
||||
}
|
||||
225
server/cmd_noauth_test.go
Normal file
225
server/cmd_noauth_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/internal"
|
||||
"github.com/emersion/go-imap/server"
|
||||
)
|
||||
|
||||
func testServerTLS(t *testing.T) (s *server.Server, c net.Conn, scanner *bufio.Scanner) {
|
||||
s, c, scanner = testServerGreeted(t)
|
||||
|
||||
cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
s.AllowInsecureAuth = false
|
||||
s.TLSConfig = tlsConfig
|
||||
|
||||
io.WriteString(c, "a001 CAPABILITY\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" STARTTLS LOGINDISABLED" {
|
||||
t.Fatal("Bad CAPABILITY response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 STARTTLS\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
sc := tls.Client(c, tlsConfig)
|
||||
if err = sc.Handshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c = sc
|
||||
scanner = bufio.NewScanner(c)
|
||||
return
|
||||
}
|
||||
|
||||
func TestStartTLS(t *testing.T) {
|
||||
s, c, scanner := testServerTLS(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CAPABILITY\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN" {
|
||||
t.Fatal("Bad CAPABILITY response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_AlreadyEnabled(t *testing.T) {
|
||||
s, c, scanner := testServerTLS(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STARTTLS\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS_NotSupported(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STARTTLS\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_Ok(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LOGIN username password\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_AlreadyAuthenticated(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LOGIN username password\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 LOGIN username password\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_No(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 LOGIN username wrongpassword\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_Plain_Ok(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "+" {
|
||||
t.Fatal("Bad continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
// :usename:password
|
||||
io.WriteString(c, "AHVzZXJuYW1lAHBhc3N3b3Jk\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_Plain_Cancel(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "+" {
|
||||
t.Fatal("Bad continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "*\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 BAD negotiation cancelled") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_Plain_InitialResponse(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_Plain_No(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 AUTHENTICATE PLAIN\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "+" {
|
||||
t.Fatal("Bad continuation request:", scanner.Text())
|
||||
}
|
||||
|
||||
// Invalid challenge
|
||||
io.WriteString(c, "BHVzZXJuYW1lAHBhc3N3b6Jk\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_No(t *testing.T) {
|
||||
s, c, scanner := testServerGreeted(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 AUTHENTICATE XIDONTEXIST\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
346
server/cmd_selected.go
Normal file
346
server/cmd_selected.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/backend"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
)
|
||||
|
||||
// imap errors in Selected state.
|
||||
var (
|
||||
ErrNoMailboxSelected = errors.New("No mailbox selected")
|
||||
ErrMailboxReadOnly = errors.New("Mailbox opened in read-only mode")
|
||||
)
|
||||
|
||||
// A command handler that supports UIDs.
|
||||
type UidHandler interface {
|
||||
Handler
|
||||
|
||||
// Handle this command using UIDs for a given connection.
|
||||
UidHandle(conn Conn) error
|
||||
}
|
||||
|
||||
type Check struct {
|
||||
commands.Check
|
||||
}
|
||||
|
||||
func (cmd *Check) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
if ctx.MailboxReadOnly {
|
||||
return ErrMailboxReadOnly
|
||||
}
|
||||
|
||||
return ctx.Mailbox.Check()
|
||||
}
|
||||
|
||||
type Close struct {
|
||||
commands.Close
|
||||
}
|
||||
|
||||
func (cmd *Close) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
|
||||
mailbox := ctx.Mailbox
|
||||
ctx.Mailbox = nil
|
||||
ctx.MailboxReadOnly = false
|
||||
|
||||
// No need to send expunge updates here, since the mailbox is already unselected
|
||||
return mailbox.Expunge()
|
||||
}
|
||||
|
||||
type Expunge struct {
|
||||
commands.Expunge
|
||||
}
|
||||
|
||||
func (cmd *Expunge) Handle(conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
if ctx.MailboxReadOnly {
|
||||
return ErrMailboxReadOnly
|
||||
}
|
||||
|
||||
// Get a list of messages that will be deleted
|
||||
// That will allow us to send expunge updates if the backend doesn't support it
|
||||
var seqnums []uint32
|
||||
if conn.Server().Updates == nil {
|
||||
criteria := &imap.SearchCriteria{
|
||||
WithFlags: []string{imap.DeletedFlag},
|
||||
}
|
||||
|
||||
var err error
|
||||
seqnums, err = ctx.Mailbox.SearchMessages(false, criteria)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctx.Mailbox.Expunge(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the backend doesn't support expunge updates, let's do it ourselves
|
||||
if conn.Server().Updates == nil {
|
||||
done := make(chan error, 1)
|
||||
|
||||
ch := make(chan uint32)
|
||||
res := &responses.Expunge{SeqNums: ch}
|
||||
|
||||
go (func() {
|
||||
done <- conn.WriteResp(res)
|
||||
// Don't need to drain 'ch', sender will stop sending when error written to 'done.
|
||||
})()
|
||||
|
||||
// Iterate sequence numbers from the last one to the first one, as deleting
|
||||
// messages changes their respective numbers
|
||||
for i := len(seqnums) - 1; i >= 0; i-- {
|
||||
// Send sequence numbers to channel, and check if conn.WriteResp() finished early.
|
||||
select {
|
||||
case ch <- seqnums[i]: // Send next seq. number
|
||||
case err := <-done: // Check for errors
|
||||
close(ch)
|
||||
return err
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Search struct {
|
||||
commands.Search
|
||||
}
|
||||
|
||||
func (cmd *Search) handle(uid bool, conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
|
||||
ids, err := ctx.Mailbox.SearchMessages(uid, cmd.Criteria)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := &responses.Search{Ids: ids}
|
||||
return conn.WriteResp(res)
|
||||
}
|
||||
|
||||
func (cmd *Search) Handle(conn Conn) error {
|
||||
return cmd.handle(false, conn)
|
||||
}
|
||||
|
||||
func (cmd *Search) UidHandle(conn Conn) error {
|
||||
return cmd.handle(true, conn)
|
||||
}
|
||||
|
||||
type Fetch struct {
|
||||
commands.Fetch
|
||||
}
|
||||
|
||||
func (cmd *Fetch) handle(uid bool, conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
|
||||
ch := make(chan *imap.Message)
|
||||
res := &responses.Fetch{Messages: ch}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go (func() {
|
||||
done <- conn.WriteResp(res)
|
||||
// Make sure to drain the message channel.
|
||||
for _ = range ch {
|
||||
}
|
||||
})()
|
||||
|
||||
err := ctx.Mailbox.ListMessages(uid, cmd.SeqSet, cmd.Items, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return <-done
|
||||
}
|
||||
|
||||
func (cmd *Fetch) Handle(conn Conn) error {
|
||||
return cmd.handle(false, conn)
|
||||
}
|
||||
|
||||
func (cmd *Fetch) UidHandle(conn Conn) error {
|
||||
// Append UID to the list of requested items if it isn't already present
|
||||
hasUid := false
|
||||
for _, item := range cmd.Items {
|
||||
if item == "UID" {
|
||||
hasUid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUid {
|
||||
cmd.Items = append(cmd.Items, "UID")
|
||||
}
|
||||
|
||||
return cmd.handle(true, conn)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
commands.Store
|
||||
}
|
||||
|
||||
func (cmd *Store) handle(uid bool, conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
if ctx.MailboxReadOnly {
|
||||
return ErrMailboxReadOnly
|
||||
}
|
||||
|
||||
// Only flags operations are supported
|
||||
op, silent, err := imap.ParseFlagsOp(cmd.Item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var flags []string
|
||||
|
||||
if flagsList, ok := cmd.Value.([]interface{}); ok {
|
||||
// Parse list of flags
|
||||
if strs, err := imap.ParseStringList(flagsList); err == nil {
|
||||
flags = strs
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Parse single flag
|
||||
if str, err := imap.ParseString(cmd.Value); err == nil {
|
||||
flags = []string{str}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i, flag := range flags {
|
||||
flags[i] = imap.CanonicalFlag(flag)
|
||||
}
|
||||
|
||||
// If the backend supports message updates, this will prevent this connection
|
||||
// from receiving them
|
||||
// TODO: find a better way to do this, without conn.silent
|
||||
*conn.silent() = silent
|
||||
err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, op, flags)
|
||||
*conn.silent() = false
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Not silent: send FETCH updates if the backend doesn't support message
|
||||
// updates
|
||||
if conn.Server().Updates == nil && !silent {
|
||||
inner := &Fetch{}
|
||||
inner.SeqSet = cmd.SeqSet
|
||||
inner.Items = []imap.FetchItem{imap.FetchFlags}
|
||||
if uid {
|
||||
inner.Items = append(inner.Items, "UID")
|
||||
}
|
||||
|
||||
if err := inner.handle(uid, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *Store) Handle(conn Conn) error {
|
||||
return cmd.handle(false, conn)
|
||||
}
|
||||
|
||||
func (cmd *Store) UidHandle(conn Conn) error {
|
||||
return cmd.handle(true, conn)
|
||||
}
|
||||
|
||||
type Copy struct {
|
||||
commands.Copy
|
||||
}
|
||||
|
||||
func (cmd *Copy) handle(uid bool, conn Conn) error {
|
||||
ctx := conn.Context()
|
||||
if ctx.Mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
|
||||
return ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox)
|
||||
}
|
||||
|
||||
func (cmd *Copy) Handle(conn Conn) error {
|
||||
return cmd.handle(false, conn)
|
||||
}
|
||||
|
||||
func (cmd *Copy) UidHandle(conn Conn) error {
|
||||
return cmd.handle(true, conn)
|
||||
}
|
||||
|
||||
type Move struct {
|
||||
commands.Move
|
||||
}
|
||||
|
||||
func (h *Move) handle(uid bool, conn Conn) error {
|
||||
mailbox := conn.Context().Mailbox
|
||||
if mailbox == nil {
|
||||
return ErrNoMailboxSelected
|
||||
}
|
||||
|
||||
if m, ok := mailbox.(backend.MoveMailbox); ok {
|
||||
return m.MoveMessages(uid, h.SeqSet, h.Mailbox)
|
||||
}
|
||||
return errors.New("MOVE extension not supported")
|
||||
}
|
||||
|
||||
func (h *Move) Handle(conn Conn) error {
|
||||
return h.handle(false, conn)
|
||||
}
|
||||
|
||||
func (h *Move) UidHandle(conn Conn) error {
|
||||
return h.handle(true, conn)
|
||||
}
|
||||
|
||||
type Uid struct {
|
||||
commands.Uid
|
||||
}
|
||||
|
||||
func (cmd *Uid) Handle(conn Conn) error {
|
||||
inner := cmd.Cmd.Command()
|
||||
hdlr, err := conn.commandHandler(inner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uidHdlr, ok := hdlr.(UidHandler)
|
||||
if !ok {
|
||||
return errors.New("Command unsupported with UID")
|
||||
}
|
||||
|
||||
if err := uidHdlr.UidHandle(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ErrStatusResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Info: "UID " + inner.Name + " completed",
|
||||
})
|
||||
}
|
||||
584
server/cmd_selected_test.go
Normal file
584
server/cmd_selected_test.go
Normal file
@@ -0,0 +1,584 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/server"
|
||||
)
|
||||
|
||||
func testServerSelected(t *testing.T, readOnly bool) (s *server.Server, c net.Conn, scanner *bufio.Scanner) {
|
||||
s, c, scanner = testServerAuthenticated(t)
|
||||
|
||||
if readOnly {
|
||||
io.WriteString(c, "a000 EXAMINE INBOX\r\n")
|
||||
} else {
|
||||
io.WriteString(c, "a000 SELECT INBOX\r\n")
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
if strings.HasPrefix(scanner.Text(), "a000 ") {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestNoop_Selected(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 NOOP\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Bad status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CHECK\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheck_ReadOnly(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CHECK\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheck_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CHECK\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CLOSE\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CLOSE\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpunge(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 EXPUNGE\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 +FLAGS.SILENT (\\Deleted)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 EXPUNGE\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 EXPUNGE" {
|
||||
t.Fatal("Invalid EXPUNGE response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpunge_ReadOnly(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 EXPUNGE\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpunge_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 EXPUNGE\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SEARCH UNDELETED\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* SEARCH 1" {
|
||||
t.Fatal("Invalid SEARCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 SEARCH DELETED\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* SEARCH" {
|
||||
t.Fatal("Invalid SEARCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 SEARCH UNDELETED\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_Uid(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 UID SEARCH UNDELETED\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* SEARCH 6" {
|
||||
t.Fatal("Invalid SEARCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetch(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 FETCH 1 (UID FLAGS)\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (UID 6 FLAGS (\\Seen))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 FETCH 1 (BODY.PEEK[TEXT])\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (BODY[TEXT] {11}" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "Hi there :))") {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetch_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 FETCH 1 (UID FLAGS)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetch_Uid(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 UID FETCH 6 (UID)\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (UID 6)" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetch_Uid_UidNotRequested(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 UID FETCH 6 (FLAGS)\r\n")
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Seen) UID 6)" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 +FLAGS (\\Flagged)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Seen \\Flagged))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 FLAGS (\\Answered)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Answered))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 -FLAGS (\\Answered)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS ())" {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 +FLAGS.SILENT (\\Flagged \\Seen)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 +FLAGS (\\Flagged)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_ReadOnly(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, true)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 +FLAGS (\\Flagged)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_InvalidOperation(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 IDONTEXIST (\\Flagged)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_InvalidFlags(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 +FLAGS ((nested)(lists))\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_SingleFlagNonList(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer c.Close()
|
||||
defer s.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 FLAGS somestring\r\n")
|
||||
|
||||
gotOK := false
|
||||
gotFetch := false
|
||||
for scanner.Scan() {
|
||||
res := scanner.Text()
|
||||
|
||||
if res == "* 1 FETCH (FLAGS (somestring))" {
|
||||
gotFetch = true
|
||||
} else if strings.HasPrefix(res, "a001 OK ") {
|
||||
gotOK = true
|
||||
break
|
||||
} else {
|
||||
t.Fatal("Unexpected response:", res)
|
||||
}
|
||||
}
|
||||
|
||||
if !gotFetch {
|
||||
t.Fatal("Missing FETCH response.")
|
||||
}
|
||||
|
||||
if !gotOK {
|
||||
t.Fatal("Missing status response.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_NonList(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer c.Close()
|
||||
defer s.Close()
|
||||
|
||||
io.WriteString(c, "a001 STORE 1 FLAGS somestring someanotherstring\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (somestring someanotherstring))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RecentFlag(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer c.Close()
|
||||
defer s.Close()
|
||||
|
||||
// Add Recent flag
|
||||
io.WriteString(c, "a001 STORE 1 FLAGS \\Recent\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Recent))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
// Set flags to: something
|
||||
// Should still get Recent flag back
|
||||
io.WriteString(c, "a001 STORE 1 FLAGS something\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Recent something))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
// Try adding Recent flag again
|
||||
io.WriteString(c, "a001 STORE 1 FLAGS \\Recent anotherflag\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Recent anotherflag))" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_Uid(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 UID STORE 6 +FLAGS (\\Flagged)\r\n")
|
||||
|
||||
scanner.Scan()
|
||||
if scanner.Text() != "* 1 FETCH (FLAGS (\\Seen \\Flagged) UID 6)" {
|
||||
t.Fatal("Invalid FETCH response:", scanner.Text())
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE CopyDest\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 COPY 1 CopyDest\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 STATUS CopyDest (MESSAGES)\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "* STATUS \"CopyDest\" (MESSAGES 1)") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy_NotSelected(t *testing.T) {
|
||||
s, c, scanner := testServerAuthenticated(t)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE CopyDest\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 COPY 1 CopyDest\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy_Uid(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 CREATE CopyDest\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 UID COPY 6 CopyDest\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 OK ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUid_InvalidCommand(t *testing.T) {
|
||||
s, c, scanner := testServerSelected(t, false)
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
|
||||
io.WriteString(c, "a001 UID IDONTEXIST\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
|
||||
io.WriteString(c, "a001 UID CLOSE\r\n")
|
||||
scanner.Scan()
|
||||
if !strings.HasPrefix(scanner.Text(), "a001 NO ") {
|
||||
t.Fatal("Invalid status response:", scanner.Text())
|
||||
}
|
||||
}
|
||||
421
server/conn.go
Normal file
421
server/conn.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/backend"
|
||||
)
|
||||
|
||||
// Conn is a connection to a client.
|
||||
type Conn interface {
|
||||
io.Reader
|
||||
|
||||
// Server returns this connection's server.
|
||||
Server() *Server
|
||||
// Context returns this connection's context.
|
||||
Context() *Context
|
||||
// Capabilities returns a list of capabilities enabled for this connection.
|
||||
Capabilities() []string
|
||||
// WriteResp writes a response to this connection.
|
||||
WriteResp(res imap.WriterTo) error
|
||||
// IsTLS returns true if TLS is enabled.
|
||||
IsTLS() bool
|
||||
// TLSState returns the TLS connection state if TLS is enabled, nil otherwise.
|
||||
TLSState() *tls.ConnectionState
|
||||
// Upgrade upgrades a connection, e.g. wrap an unencrypted connection with an
|
||||
// encrypted tunnel.
|
||||
Upgrade(upgrader imap.ConnUpgrader) error
|
||||
// Close closes this connection.
|
||||
Close() error
|
||||
WaitReady()
|
||||
|
||||
Info() *imap.ConnInfo
|
||||
|
||||
setTLSConn(*tls.Conn)
|
||||
silent() *bool // TODO: remove this
|
||||
serve(Conn) error
|
||||
commandHandler(cmd *imap.Command) (hdlr Handler, err error)
|
||||
}
|
||||
|
||||
// Context stores a connection's metadata.
|
||||
type Context struct {
|
||||
// This connection's current state.
|
||||
State imap.ConnState
|
||||
// If the client is logged in, the user.
|
||||
User backend.User
|
||||
// If the client has selected a mailbox, the mailbox.
|
||||
Mailbox backend.Mailbox
|
||||
// True if the currently selected mailbox has been opened in read-only mode.
|
||||
MailboxReadOnly bool
|
||||
// Responses to send to the client.
|
||||
Responses chan<- imap.WriterTo
|
||||
// Closed when the client is logged out.
|
||||
LoggedOut <-chan struct{}
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
*imap.Conn
|
||||
|
||||
conn Conn // With extensions overrides
|
||||
s *Server
|
||||
ctx *Context
|
||||
tlsConn *tls.Conn
|
||||
continues chan bool
|
||||
upgrade chan bool
|
||||
responses chan imap.WriterTo
|
||||
loggedOut chan struct{}
|
||||
silentVal bool
|
||||
}
|
||||
|
||||
func newConn(s *Server, c net.Conn) *conn {
|
||||
// Create an imap.Reader and an imap.Writer
|
||||
continues := make(chan bool)
|
||||
r := imap.NewServerReader(nil, continues)
|
||||
w := imap.NewWriter(nil)
|
||||
|
||||
responses := make(chan imap.WriterTo)
|
||||
loggedOut := make(chan struct{})
|
||||
|
||||
tlsConn, _ := c.(*tls.Conn)
|
||||
|
||||
conn := &conn{
|
||||
Conn: imap.NewConn(c, r, w),
|
||||
|
||||
s: s,
|
||||
ctx: &Context{
|
||||
State: imap.ConnectingState,
|
||||
Responses: responses,
|
||||
LoggedOut: loggedOut,
|
||||
},
|
||||
tlsConn: tlsConn,
|
||||
continues: continues,
|
||||
upgrade: make(chan bool),
|
||||
responses: responses,
|
||||
loggedOut: loggedOut,
|
||||
}
|
||||
|
||||
if s.Debug != nil {
|
||||
conn.Conn.SetDebug(s.Debug)
|
||||
}
|
||||
if s.MaxLiteralSize > 0 {
|
||||
conn.Conn.MaxLiteralSize = s.MaxLiteralSize
|
||||
}
|
||||
|
||||
go conn.send()
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (c *conn) Server() *Server {
|
||||
return c.s
|
||||
}
|
||||
|
||||
func (c *conn) Context() *Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
type response struct {
|
||||
response imap.WriterTo
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (r *response) WriteTo(w *imap.Writer) error {
|
||||
err := r.response.WriteTo(w)
|
||||
close(r.done)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) setDeadline() {
|
||||
if c.s.AutoLogout == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
dur := c.s.AutoLogout
|
||||
if dur < MinAutoLogout {
|
||||
dur = MinAutoLogout
|
||||
}
|
||||
t := time.Now().Add(dur)
|
||||
|
||||
c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *conn) WriteResp(r imap.WriterTo) error {
|
||||
done := make(chan struct{})
|
||||
c.responses <- &response{r, done}
|
||||
<-done
|
||||
c.setDeadline()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Close() error {
|
||||
if c.ctx.User != nil {
|
||||
c.ctx.User.Logout()
|
||||
}
|
||||
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *conn) Capabilities() []string {
|
||||
caps := []string{"IMAP4rev1", "LITERAL+", "SASL-IR", "CHILDREN", "UNSELECT", "MOVE", "IDLE"}
|
||||
|
||||
appendLimitSet := false
|
||||
if c.ctx.State == imap.AuthenticatedState {
|
||||
if u, ok := c.ctx.User.(backend.AppendLimitUser); ok {
|
||||
if limit := u.CreateMessageLimit(); limit != nil {
|
||||
caps = append(caps, fmt.Sprintf("APPENDLIMIT=%v", *limit))
|
||||
appendLimitSet = true
|
||||
}
|
||||
}
|
||||
} else if be, ok := c.Server().Backend.(backend.AppendLimitBackend); ok {
|
||||
if limit := be.CreateMessageLimit(); limit != nil {
|
||||
caps = append(caps, fmt.Sprintf("APPENDLIMIT=%v", *limit))
|
||||
appendLimitSet = true
|
||||
}
|
||||
}
|
||||
if !appendLimitSet {
|
||||
caps = append(caps, "APPENDLIMIT")
|
||||
}
|
||||
|
||||
if c.ctx.State == imap.NotAuthenticatedState {
|
||||
if !c.IsTLS() && c.s.TLSConfig != nil {
|
||||
caps = append(caps, "STARTTLS")
|
||||
}
|
||||
|
||||
if !c.canAuth() {
|
||||
caps = append(caps, "LOGINDISABLED")
|
||||
} else {
|
||||
for name := range c.s.auths {
|
||||
caps = append(caps, "AUTH="+name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range c.s.extensions {
|
||||
caps = append(caps, ext.Capabilities(c)...)
|
||||
}
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
func (c *conn) writeAndFlush(w imap.WriterTo) error {
|
||||
if err := w.WriteTo(c.Writer); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Writer.Flush()
|
||||
}
|
||||
|
||||
func (c *conn) send() {
|
||||
// Send responses
|
||||
for {
|
||||
select {
|
||||
case <-c.upgrade:
|
||||
// Wait until upgrade is finished.
|
||||
c.Wait()
|
||||
case needCont := <-c.continues:
|
||||
// Send continuation requests
|
||||
if needCont {
|
||||
resp := &imap.ContinuationReq{Info: "send literal"}
|
||||
if err := c.writeAndFlush(resp); err != nil {
|
||||
c.Server().ErrorLog.Println("cannot send continuation request: ", err)
|
||||
}
|
||||
}
|
||||
case res := <-c.responses:
|
||||
// Got a response that needs to be sent
|
||||
// Request to send the response
|
||||
if err := c.writeAndFlush(res); err != nil {
|
||||
c.Server().ErrorLog.Println("cannot send response: ", err)
|
||||
}
|
||||
case <-c.loggedOut:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) greet() error {
|
||||
c.ctx.State = imap.NotAuthenticatedState
|
||||
|
||||
caps := c.Capabilities()
|
||||
args := make([]interface{}, len(caps))
|
||||
for i, cap := range caps {
|
||||
args[i] = imap.RawString(cap)
|
||||
}
|
||||
|
||||
greeting := &imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
Code: imap.CodeCapability,
|
||||
Arguments: args,
|
||||
Info: "IMAP4rev1 Service Ready",
|
||||
}
|
||||
|
||||
return c.WriteResp(greeting)
|
||||
}
|
||||
|
||||
func (c *conn) setTLSConn(tlsConn *tls.Conn) {
|
||||
c.tlsConn = tlsConn
|
||||
}
|
||||
|
||||
func (c *conn) IsTLS() bool {
|
||||
return c.tlsConn != nil
|
||||
}
|
||||
|
||||
func (c *conn) TLSState() *tls.ConnectionState {
|
||||
if c.tlsConn != nil {
|
||||
state := c.tlsConn.ConnectionState()
|
||||
return &state
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// canAuth checks if the client can use plain text authentication.
|
||||
func (c *conn) canAuth() bool {
|
||||
return c.IsTLS() || c.s.AllowInsecureAuth
|
||||
}
|
||||
|
||||
func (c *conn) silent() *bool {
|
||||
return &c.silentVal
|
||||
}
|
||||
|
||||
func (c *conn) serve(conn Conn) (err error) {
|
||||
c.conn = conn
|
||||
|
||||
defer func() {
|
||||
c.ctx.State = imap.LogoutState
|
||||
close(c.loggedOut)
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.WriteResp(&imap.StatusResp{
|
||||
Type: imap.StatusRespBye,
|
||||
Info: "Internal server error, closing connection.",
|
||||
})
|
||||
|
||||
stack := debug.Stack()
|
||||
c.s.ErrorLog.Printf("panic serving %v: %v\n%s", c.Info().RemoteAddr, r, stack)
|
||||
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Send greeting
|
||||
if err := c.greet(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
if c.ctx.State == imap.LogoutState {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res *imap.StatusResp
|
||||
var up Upgrader
|
||||
|
||||
fields, err := c.ReadLine()
|
||||
if err == io.EOF || c.ctx.State == imap.LogoutState {
|
||||
return nil
|
||||
}
|
||||
c.setDeadline()
|
||||
|
||||
if err != nil {
|
||||
if imap.IsParseError(err) {
|
||||
res = &imap.StatusResp{
|
||||
Type: imap.StatusRespBad,
|
||||
Info: err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.s.ErrorLog.Println("cannot read command:", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cmd := &imap.Command{}
|
||||
if err := cmd.Parse(fields); err != nil {
|
||||
res = &imap.StatusResp{
|
||||
Tag: cmd.Tag,
|
||||
Type: imap.StatusRespBad,
|
||||
Info: err.Error(),
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
res, up, err = c.handleCommand(cmd)
|
||||
if err != nil {
|
||||
res = &imap.StatusResp{
|
||||
Tag: cmd.Tag,
|
||||
Type: imap.StatusRespBad,
|
||||
Info: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
|
||||
if err := c.WriteResp(res); err != nil {
|
||||
c.s.ErrorLog.Println("cannot write response:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if up != nil && res.Type == imap.StatusRespOk {
|
||||
if err := up.Upgrade(c.conn); err != nil {
|
||||
c.s.ErrorLog.Println("cannot upgrade connection:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) WaitReady() {
|
||||
c.upgrade <- true
|
||||
c.Conn.WaitReady()
|
||||
}
|
||||
|
||||
func (c *conn) commandHandler(cmd *imap.Command) (hdlr Handler, err error) {
|
||||
newHandler := c.s.Command(cmd.Name)
|
||||
if newHandler == nil {
|
||||
err = errors.New("Unknown command")
|
||||
return
|
||||
}
|
||||
|
||||
hdlr = newHandler()
|
||||
err = hdlr.Parse(cmd.Arguments)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrader, err error) {
|
||||
hdlr, err := c.commandHandler(cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hdlrErr := hdlr.Handle(c.conn)
|
||||
if statusErr, ok := hdlrErr.(*imap.ErrStatusResp); ok {
|
||||
res = statusErr.Resp
|
||||
} else if hdlrErr != nil {
|
||||
res = &imap.StatusResp{
|
||||
Type: imap.StatusRespNo,
|
||||
Info: hdlrErr.Error(),
|
||||
}
|
||||
} else {
|
||||
res = &imap.StatusResp{
|
||||
Type: imap.StatusRespOk,
|
||||
}
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
res.Tag = cmd.Tag
|
||||
|
||||
if res.Type == imap.StatusRespOk && res.Info == "" {
|
||||
res.Info = cmd.Name + " completed"
|
||||
}
|
||||
}
|
||||
|
||||
up, _ = hdlr.(Upgrader)
|
||||
return
|
||||
}
|
||||
419
server/server.go
Normal file
419
server/server.go
Normal file
@@ -0,0 +1,419 @@
|
||||
// Package server provides an IMAP server.
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/backend"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
// The minimum autologout duration defined in RFC 3501 section 5.4.
|
||||
const MinAutoLogout = 30 * time.Minute
|
||||
|
||||
// A command handler.
|
||||
type Handler interface {
|
||||
imap.Parser
|
||||
|
||||
// Handle this command for a given connection.
|
||||
//
|
||||
// By default, after this function has returned a status response is sent. To
|
||||
// prevent this behavior handlers can use imap.ErrStatusResp.
|
||||
Handle(conn Conn) error
|
||||
}
|
||||
|
||||
// A connection upgrader. If a Handler is also an Upgrader, the connection will
|
||||
// be upgraded after the Handler succeeds.
|
||||
//
|
||||
// This should only be used by libraries implementing an IMAP extension (e.g.
|
||||
// COMPRESS).
|
||||
type Upgrader interface {
|
||||
// Upgrade the connection. This method should call conn.Upgrade().
|
||||
Upgrade(conn Conn) error
|
||||
}
|
||||
|
||||
// A function that creates handlers.
|
||||
type HandlerFactory func() Handler
|
||||
|
||||
// A function that creates SASL servers.
|
||||
type SASLServerFactory func(conn Conn) sasl.Server
|
||||
|
||||
// An IMAP extension.
|
||||
type Extension interface {
|
||||
// Get capabilities provided by this extension for a given connection.
|
||||
Capabilities(c Conn) []string
|
||||
// Get the command handler factory for the provided command name.
|
||||
Command(name string) HandlerFactory
|
||||
}
|
||||
|
||||
// An extension that provides additional features to each connection.
|
||||
type ConnExtension interface {
|
||||
Extension
|
||||
|
||||
// This function will be called when a client connects to the server. It can
|
||||
// be used to add new features to the default Conn interface by implementing
|
||||
// new methods.
|
||||
NewConn(c Conn) Conn
|
||||
}
|
||||
|
||||
// ErrStatusResp can be returned by a Handler to replace the default status
|
||||
// response. The response tag must be empty.
|
||||
//
|
||||
// Deprecated: Use imap.ErrStatusResp{res} instead.
|
||||
//
|
||||
// To disable the default status response, use imap.ErrStatusResp{nil} instead.
|
||||
func ErrStatusResp(res *imap.StatusResp) error {
|
||||
return &imap.ErrStatusResp{res}
|
||||
}
|
||||
|
||||
// ErrNoStatusResp can be returned by a Handler to prevent the default status
|
||||
// response from being sent.
|
||||
//
|
||||
// Deprecated: Use imap.ErrStatusResp{nil} instead
|
||||
func ErrNoStatusResp() error {
|
||||
return &imap.ErrStatusResp{nil}
|
||||
}
|
||||
|
||||
// An IMAP server.
|
||||
type Server struct {
|
||||
locker sync.Mutex
|
||||
listeners map[net.Listener]struct{}
|
||||
conns map[Conn]struct{}
|
||||
|
||||
commands map[string]HandlerFactory
|
||||
auths map[string]SASLServerFactory
|
||||
extensions []Extension
|
||||
|
||||
// TCP address to listen on.
|
||||
Addr string
|
||||
// This server's TLS configuration.
|
||||
TLSConfig *tls.Config
|
||||
// This server's backend.
|
||||
Backend backend.Backend
|
||||
// Backend updates that will be sent to connected clients.
|
||||
Updates <-chan backend.Update
|
||||
// Automatically logout clients after a duration. To do not logout users
|
||||
// automatically, set this to zero. The duration MUST be at least
|
||||
// MinAutoLogout (as stated in RFC 3501 section 5.4).
|
||||
AutoLogout time.Duration
|
||||
// Allow authentication over unencrypted connections.
|
||||
AllowInsecureAuth bool
|
||||
// An io.Writer to which all network activity will be mirrored.
|
||||
Debug io.Writer
|
||||
// ErrorLog specifies an optional logger for errors accepting
|
||||
// connections and unexpected behavior from handlers.
|
||||
// If nil, logging goes to os.Stderr via the log package's
|
||||
// standard logger.
|
||||
ErrorLog imap.Logger
|
||||
// The maximum literal size, in bytes. Literals exceeding this size will be
|
||||
// rejected. A value of zero disables the limit (this is the default).
|
||||
MaxLiteralSize uint32
|
||||
}
|
||||
|
||||
// Create a new IMAP server from an existing listener.
|
||||
func New(bkd backend.Backend) *Server {
|
||||
s := &Server{
|
||||
listeners: make(map[net.Listener]struct{}),
|
||||
conns: make(map[Conn]struct{}),
|
||||
Backend: bkd,
|
||||
ErrorLog: log.New(os.Stderr, "imap/server: ", log.LstdFlags),
|
||||
}
|
||||
|
||||
s.auths = map[string]SASLServerFactory{
|
||||
sasl.Plain: func(conn Conn) sasl.Server {
|
||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
if identity != "" && identity != username {
|
||||
return errors.New("Identities not supported")
|
||||
}
|
||||
|
||||
user, err := bkd.Login(conn.Info(), username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := conn.Context()
|
||||
ctx.State = imap.AuthenticatedState
|
||||
ctx.User = user
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
s.commands = map[string]HandlerFactory{
|
||||
"NOOP": func() Handler { return &Noop{} },
|
||||
"CAPABILITY": func() Handler { return &Capability{} },
|
||||
"LOGOUT": func() Handler { return &Logout{} },
|
||||
|
||||
"STARTTLS": func() Handler { return &StartTLS{} },
|
||||
"LOGIN": func() Handler { return &Login{} },
|
||||
"AUTHENTICATE": func() Handler { return &Authenticate{} },
|
||||
|
||||
"SELECT": func() Handler { return &Select{} },
|
||||
"EXAMINE": func() Handler {
|
||||
hdlr := &Select{}
|
||||
hdlr.ReadOnly = true
|
||||
return hdlr
|
||||
},
|
||||
"CREATE": func() Handler { return &Create{} },
|
||||
"DELETE": func() Handler { return &Delete{} },
|
||||
"RENAME": func() Handler { return &Rename{} },
|
||||
"SUBSCRIBE": func() Handler { return &Subscribe{} },
|
||||
"UNSUBSCRIBE": func() Handler { return &Unsubscribe{} },
|
||||
"LIST": func() Handler { return &List{} },
|
||||
"LSUB": func() Handler {
|
||||
hdlr := &List{}
|
||||
hdlr.Subscribed = true
|
||||
return hdlr
|
||||
},
|
||||
"STATUS": func() Handler { return &Status{} },
|
||||
"APPEND": func() Handler { return &Append{} },
|
||||
"UNSELECT": func() Handler { return &Unselect{} },
|
||||
"IDLE": func() Handler { return &Idle{} },
|
||||
|
||||
"CHECK": func() Handler { return &Check{} },
|
||||
"CLOSE": func() Handler { return &Close{} },
|
||||
"EXPUNGE": func() Handler { return &Expunge{} },
|
||||
"SEARCH": func() Handler { return &Search{} },
|
||||
"FETCH": func() Handler { return &Fetch{} },
|
||||
"STORE": func() Handler { return &Store{} },
|
||||
"COPY": func() Handler { return &Copy{} },
|
||||
"MOVE": func() Handler { return &Move{} },
|
||||
"UID": func() Handler { return &Uid{} },
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the Listener l.
|
||||
func (s *Server) Serve(l net.Listener) error {
|
||||
s.locker.Lock()
|
||||
s.listeners[l] = struct{}{}
|
||||
s.locker.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
l.Close()
|
||||
delete(s.listeners, l)
|
||||
}()
|
||||
|
||||
updater, ok := s.Backend.(backend.BackendUpdater)
|
||||
if ok {
|
||||
s.Updates = updater.Updates()
|
||||
go s.listenUpdates()
|
||||
}
|
||||
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var conn Conn = newConn(s, c)
|
||||
for _, ext := range s.extensions {
|
||||
if ext, ok := ext.(ConnExtension); ok {
|
||||
conn = ext.NewConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
go s.serveConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the TCP network address s.Addr and then calls Serve
|
||||
// to handle requests on incoming connections.
|
||||
//
|
||||
// If s.Addr is blank, ":imap" is used.
|
||||
func (s *Server) ListenAndServe() error {
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
addr = ":imap"
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Serve(l)
|
||||
}
|
||||
|
||||
// ListenAndServeTLS listens on the TCP network address s.Addr and then calls
|
||||
// Serve to handle requests on incoming TLS connections.
|
||||
//
|
||||
// If s.Addr is blank, ":imaps" is used.
|
||||
func (s *Server) ListenAndServeTLS() error {
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
addr = ":imaps"
|
||||
}
|
||||
|
||||
l, err := tls.Listen("tcp", addr, s.TLSConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Serve(l)
|
||||
}
|
||||
|
||||
func (s *Server) serveConn(conn Conn) error {
|
||||
s.locker.Lock()
|
||||
s.conns[conn] = struct{}{}
|
||||
s.locker.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
conn.Close()
|
||||
delete(s.conns, conn)
|
||||
}()
|
||||
|
||||
return conn.serve(conn)
|
||||
}
|
||||
|
||||
// Command gets a command handler factory for the provided command name.
|
||||
func (s *Server) Command(name string) HandlerFactory {
|
||||
// Extensions can override builtin commands
|
||||
for _, ext := range s.extensions {
|
||||
if h := ext.Command(name); h != nil {
|
||||
return h
|
||||
}
|
||||
}
|
||||
|
||||
return s.commands[name]
|
||||
}
|
||||
|
||||
func (s *Server) listenUpdates() {
|
||||
for {
|
||||
update := <-s.Updates
|
||||
|
||||
var res imap.WriterTo
|
||||
switch update := update.(type) {
|
||||
case *backend.StatusUpdate:
|
||||
res = update.StatusResp
|
||||
case *backend.MailboxUpdate:
|
||||
res = &responses.Select{Mailbox: update.MailboxStatus}
|
||||
case *backend.MailboxInfoUpdate:
|
||||
ch := make(chan *imap.MailboxInfo, 1)
|
||||
ch <- update.MailboxInfo
|
||||
close(ch)
|
||||
|
||||
res = &responses.List{Mailboxes: ch}
|
||||
case *backend.MessageUpdate:
|
||||
ch := make(chan *imap.Message, 1)
|
||||
ch <- update.Message
|
||||
close(ch)
|
||||
|
||||
res = &responses.Fetch{Messages: ch}
|
||||
case *backend.ExpungeUpdate:
|
||||
ch := make(chan uint32, 1)
|
||||
ch <- update.SeqNum
|
||||
close(ch)
|
||||
|
||||
res = &responses.Expunge{SeqNums: ch}
|
||||
default:
|
||||
s.ErrorLog.Printf("unhandled update: %T\n", update)
|
||||
}
|
||||
if res == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sends := make(chan struct{})
|
||||
wait := 0
|
||||
s.locker.Lock()
|
||||
for conn := range s.conns {
|
||||
ctx := conn.Context()
|
||||
|
||||
if update.Username() != "" && (ctx.User == nil || ctx.User.Username() != update.Username()) {
|
||||
continue
|
||||
}
|
||||
if update.Mailbox() != "" && (ctx.Mailbox == nil || ctx.Mailbox.Name() != update.Mailbox()) {
|
||||
continue
|
||||
}
|
||||
if *conn.silent() {
|
||||
// If silent is set, do not send message updates
|
||||
if _, ok := res.(*responses.Fetch); ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
conn := conn // Copy conn to a local variable
|
||||
go func() {
|
||||
done := make(chan struct{})
|
||||
conn.Context().Responses <- &response{
|
||||
response: res,
|
||||
done: done,
|
||||
}
|
||||
<-done
|
||||
sends <- struct{}{}
|
||||
}()
|
||||
|
||||
wait++
|
||||
}
|
||||
s.locker.Unlock()
|
||||
|
||||
if wait > 0 {
|
||||
go func() {
|
||||
for done := 0; done < wait; done++ {
|
||||
<-sends
|
||||
}
|
||||
|
||||
close(update.Done())
|
||||
}()
|
||||
} else {
|
||||
close(update.Done())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ForEachConn iterates through all opened connections.
|
||||
func (s *Server) ForEachConn(f func(Conn)) {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
for conn := range s.conns {
|
||||
f(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Stops listening and closes all current connections.
|
||||
func (s *Server) Close() error {
|
||||
s.locker.Lock()
|
||||
defer s.locker.Unlock()
|
||||
|
||||
for l := range s.listeners {
|
||||
l.Close()
|
||||
}
|
||||
|
||||
for conn := range s.conns {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable some IMAP extensions on this server.
|
||||
// Wiki entry: https://github.com/emersion/go-imap/wiki/Using-extensions
|
||||
func (s *Server) Enable(extensions ...Extension) {
|
||||
for _, ext := range extensions {
|
||||
// Ignore built-in extensions
|
||||
if ext.Command("UNSELECT") != nil || ext.Command("MOVE") != nil || ext.Command("IDLE") != nil {
|
||||
continue
|
||||
}
|
||||
s.extensions = append(s.extensions, ext)
|
||||
}
|
||||
}
|
||||
|
||||
// Enable an authentication mechanism on this server.
|
||||
// Wiki entry: https://github.com/emersion/go-imap/wiki/Using-authentication-mechanisms
|
||||
func (s *Server) EnableAuth(name string, f SASLServerFactory) {
|
||||
s.auths[name] = f
|
||||
}
|
||||
49
server/server_test.go
Normal file
49
server/server_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/backend/memory"
|
||||
"github.com/emersion/go-imap/server"
|
||||
)
|
||||
|
||||
// Extnesions that are always advertised by go-imap server.
|
||||
const builtinExtensions = "LITERAL+ SASL-IR CHILDREN UNSELECT MOVE IDLE APPENDLIMIT"
|
||||
|
||||
func testServer(t *testing.T) (s *server.Server, conn net.Conn) {
|
||||
bkd := memory.New()
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal("Cannot listen:", err)
|
||||
}
|
||||
|
||||
s = server.New(bkd)
|
||||
s.AllowInsecureAuth = true
|
||||
|
||||
go s.Serve(l)
|
||||
|
||||
conn, err = net.Dial("tcp", l.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to server:", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestServer_greeting(t *testing.T) {
|
||||
s, conn := testServer(t)
|
||||
defer s.Close()
|
||||
defer conn.Close()
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
|
||||
scanner.Scan() // Wait for greeting
|
||||
greeting := scanner.Text()
|
||||
|
||||
if greeting != "* OK [CAPABILITY IMAP4rev1 "+builtinExtensions+" AUTH=PLAIN] IMAP4rev1 Service Ready" {
|
||||
t.Fatal("Bad greeting:", greeting)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user