This is the v1 version, had the v2 before.

This commit is contained in:
2025-05-01 12:02:19 +03:00
parent bcc3f95e8e
commit 0e253ba422
130 changed files with 18061 additions and 2179 deletions

52
server/cmd_any.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
}