view managesieve_test.go @ 12:66b46b3d73be default tip

Handle capabilities sent by the server after negotiating a SASL security layer
author Guido Berhoerster <guido+managesieve@berhoerster.name>
date Tue, 09 Feb 2021 23:01:02 +0100
parents b790df0733d4
children
line wrap: on
line source

// Copyright (C) 2020 Guido Berhoerster <guido+managesieve@berhoerster.name>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package managesieve_test

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net"
	"strings"
	"testing"
	"time"

	"go.guido-berhoerster.org/managesieve"
)

func quoteClientString(s string) string {
	return fmt.Sprintf("{%d+}\r\n%s", len(s), s)
}

func quoteServerString(s string) string {
	return fmt.Sprintf("{%d}\r\n%s", len(s), s)
}

// turn LF into CRLF line endeings and substitute passed key-value string pairs
func fixClientOutput(s string, replacements ...string) string {
	return strings.NewReplacer(append(replacements,
		"\n", "\r\n")...).Replace(s)
}

type fakeConn struct {
	io.ReadWriter
	buf bytes.Buffer
	w   *bufio.Writer
}

func newFakeConn(s string) *fakeConn {
	f := &fakeConn{}
	r := bufio.NewReader(strings.NewReader(s))
	f.w = bufio.NewWriter(&f.buf)
	f.ReadWriter = bufio.NewReadWriter(r, f.w)
	return f
}

func (f *fakeConn) Close() error {
	return nil
}

func (f *fakeConn) LocalAddr() net.Addr {
	return nil
}

func (f *fakeConn) RemoteAddr() net.Addr {
	return nil
}

func (f *fakeConn) SetDeadline(time.Time) error {
	return nil
}

func (f *fakeConn) SetReadDeadline(time.Time) error {
	return nil
}

func (f *fakeConn) SetWriteDeadline(time.Time) error {
	return nil
}

func (f *fakeConn) Written() []byte {
	f.w.Flush()
	return f.buf.Bytes()
}

// basic test of net-unicode validation
func TestNetUnicode(t *testing.T) {
	if !managesieve.IsNetUnicode("abc\u00a9") {
		t.Fatalf("expected valid net-unicode")
	}
	if managesieve.IsNetUnicode("a\tbc") {
		t.Fatalf("expected invalid net-unicode")
	}
	if managesieve.IsNetUnicode("a\u0080bc") {
		t.Fatalf("expected invalid net-unicode")
	}
	if managesieve.IsNetUnicode("a\u2028bc") {
		t.Fatalf("expected invalid net-unicode")
	}
}

var validScript string = `redirect "111@example.net";

if size :under 10k {
    redirect "mobile@cell.example.com";
}

if envelope :contains "to" "tmartin+lists" {
    redirect "lists@groups.example.com";
}
`
var expectedWarnings = "line 8: server redirect action limit is 2, this redirect might be ignored"

// basic functionality
var basicServer string = `"IMPlemENTATION" "Example1 ManageSieved v001"
"SASl" "PLAIN DIGEST-MD5 GSSAPI"
"SIeVE" "fileinto vacation"
"StaRTTLS"
"NOTIFY" "xmpp mailto"
"MAXREdIRECTS" "5"
"VERSION" "1.0"
OK
OK
"IMPlemENTATION" "Example1 ManageSieved v001"
"SASl" "PLAIN DIGEST-MD5 GSSAPI"
"SIeVE" "fileinto vacation"
"StaRTTLS"
"NOTIFY" "xmpp mailto"
"MAXREdIRECTS" "5"
"VERSION" "1.0"
OK
OK (WARNINGS) "` + expectedWarnings + `"
OK
OK (WARNINGS) "` + expectedWarnings + `"
OK
"default" ACTIVE
OK
` + quoteServerString(validScript) + `
OK
OK
OK
OK
OK
OK
`

var basicExpectedOutput string = fixClientOutput(`AUTHENTICATE "PLAIN" "AGZvbwBTM2NSM1Q="
CAPABILITY
CHECKSCRIPT @quotedScript@
HAVESPACE {7+}
default @scriptLength@
PUTSCRIPT {7+}
default @quotedScript@
SETACTIVE {7+}
default
LISTSCRIPTS
GETSCRIPT {7+}
default
SETACTIVE {0+}

RENAMESCRIPT {7+}
default {4+}
test
DELETESCRIPT {4+}
test
NOOP
LOGOUT
`,
	"@quotedScript@", quoteClientString(validScript),
	"@scriptLength@", fmt.Sprint(len(validScript)))

func TestBasicFunctionality(t *testing.T) {
	conn := newFakeConn(basicServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	if !c.SupportsRFC5804() {
		t.Fatal("RFC5804 support unexpectedly not detected")
	}

	if !c.SupportsTLS() {
		t.Fatal("STARTTLS support unexpectedly not detected")
	}

	if ext := c.Extensions(); strings.Join(ext, " ") !=
		"fileinto vacation" {
		t.Fatalf("expected extensions: [fileinto vacation], got: %v",
			ext)
	}

	if notify := c.NotifyMethods(); strings.Join(notify, " ") !=
		"xmpp mailto" {
		t.Fatalf("expected notify methods: [xmpp mailto], got: %v",
			notify)
	}

	if redir := c.MaxRedirects(); redir != 5 {
		t.Fatalf("expected max redirects: 5 got: %d", redir)
	}

	if strings.Join(c.SASLMechanisms(), " ") != "PLAIN DIGEST-MD5 GSSAPI" {
		t.Fatal("failed check SASL methods")
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	if err := c.Authenticate(auth); err != nil {
		t.Fatalf("plain authentication failed: %s", err)
	}

	if warnings, err := c.CheckScript(validScript); err != nil {
		t.Fatalf("CHECKSCRIPT failed: %s", err)
	} else if warnings != expectedWarnings {
		t.Fatalf("CHECKSCRIPT expected: %s, got %s", warnings,
			expectedWarnings)
	}

	if ok, err := c.HaveSpace("default", int64(len(validScript))); err != nil {
		t.Fatalf("HAVESPACE failed: %s", err)
	} else if !ok {
		t.Fatal("HaveSpace unexpectedly returned false")
	}

	if warnings, err := c.PutScript("default", validScript); err != nil {
		t.Fatalf("PUTSCRIPT failed: %s", err)
	} else if warnings != expectedWarnings {
		t.Fatalf("PUTSCRIPT expected: %s, got %s", warnings,
			expectedWarnings)
	}

	if err = c.ActivateScript("default"); err != nil {
		t.Fatalf("SETACTIVE failed: %s", err)
	}

	if scripts, active, err := c.ListScripts(); err != nil {
		t.Fatalf("LISTSCRIPTS failed: %s", err)
	} else if active != "default" {
		t.Fatalf("failed to get active script")
	} else if len(scripts) != 1 || scripts[0] != "default" {
		t.Fatal("failed to get scripts")
	}

	if recievedScript, err := c.GetScript("default"); err != nil {
		t.Fatalf("GETSCRIPT failed: %s", err)
	} else if recievedScript != validScript {
		t.Fatalf("GETSCRIPT expected:\n%s\ngot:\n%s\n",
			validScript, recievedScript)
	}

	if err = c.ActivateScript(""); err != nil {
		t.Fatalf("SETACTIVE failed: %s", err)
	}

	if err = c.RenameScript("default", "test"); err != nil {
		t.Fatalf("RENAMESCRIPT failed: %s", err)
	}

	if err = c.DeleteScript("test"); err != nil {
		t.Fatalf("DELETESCRIPT failed: %s", err)
	}

	if err = c.Noop(); err != nil {
		t.Fatalf("NOOP failed unexpectedly: %s", err)
	}

	if err = c.Logout(); err != nil {
		t.Fatalf("failed to log out: %s", err)
	}

	clientOutput := string(conn.Written())
	if clientOutput != basicExpectedOutput {
		t.Fatalf("expected:\n%s\ngot:\n%s\n", basicExpectedOutput,
			clientOutput)
	}
}

// unexpected EOF after length in literal string
func TestUnexpectedEOFInLiteral(t *testing.T) {
	conn := newFakeConn(`"IMPLEMENTATION" {10}`)
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if err != io.ErrUnexpectedEOF {
		t.Fatalf("expected io.ErrUnexpectedEOF, got %T (%q)", err, err)
	}
}

// CR without LF after length in literal string
func TestInvalidCRInLiteral(t *testing.T) {
	conn := newFakeConn("{2}\rXX\nOK\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// overlong literal string
func TestOverlongLiteral(t *testing.T) {
	conn := newFakeConn(fmt.Sprintf("{%d}\r\n", managesieve.ReadLimit+1))
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// string in place of an response code atom
func TestInvalidResponseCode(t *testing.T) {
	conn := newFakeConn("NO (\"XXX\")\r\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// EOF after response code
func TestResponseUnexpectedEOF(t *testing.T) {
	conn := newFakeConn("NO (XXX")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if err != io.ErrUnexpectedEOF {
		t.Fatalf("expected io.ErrUnexpectedEOF, got %T (%q)", err, err)
	}
}

// invalid atom in place of the response message string
func TestInvalidResponseMessage(t *testing.T) {
	conn := newFakeConn("BYE XXX\r\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// trailing invalid atom after response message
func TestResponseTrailingData(t *testing.T) {
	conn := newFakeConn("BYE \"XXX\" XXX\r\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// capabilities with atom instead of key string
func TestCapabilitiesInvalidKey(t *testing.T) {
	conn := newFakeConn("IMPLEMENTATION \"Example\"\r\nOK\r\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// capabilities with atom instead of value string
func TestCapabilitiesInvalidValue(t *testing.T) {
	conn := newFakeConn("\"IMPLEMENTATION\" Example\r\nOK\r\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

var unexpectedResponseServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
`

// unexpected EOF
func TestUnexpectedEOF(t *testing.T) {
	conn := newFakeConn(unexpectedResponseServer)
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if err != io.ErrUnexpectedEOF {
		t.Fatalf("expected io.ErrUnexpectedEOF, got %T (%q)", err, err)
	}
}

// unexpected NO
func TestUnexpectedNo(t *testing.T) {
	conn := newFakeConn(unexpectedResponseServer + "NO\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(*managesieve.ServerError); !ok {
		t.Fatalf("expected *managesieve.ServerError, got %T (%q)", err,
			err)
	}
}

// unexpected BYE
func TestUnexpectedBye(t *testing.T) {
	conn := newFakeConn(unexpectedResponseServer + "BYE\n")
	_, err := managesieve.NewClient(conn, "imap.example.net")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(*managesieve.ConnClosedError); !ok {
		t.Fatalf("expected *managesieve.ConnClosedError, got %T (%q)",
			err, err)
	}
}

var invalidMaxRedirectsServer = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"MAxREDIRECTS" "-1"
"VERSION" "1.0"
OK
`

func TestInvalidMaxRedirects(t *testing.T) {
	conn := newFakeConn(invalidMaxRedirectsServer)
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	if redir := c.MaxRedirects(); redir != 0 {
		t.Fatalf("invalid MAXREDIRECTS, expected 0, got %d", redir)
	}
}

var minimalServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
`

var haveSpaceServer string = minimalServer + `NO
NO (QUOTA)
NO (QUOTA/MAXSIZE) "Quota exceeded"
`

// handling of unknown errors and quota exceeded errors
func TestHaveSpace(t *testing.T) {
	conn := newFakeConn(haveSpaceServer)
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	var ok bool
	// unknown error response
	ok, err = c.HaveSpace("foo", 999999)
	if ok {
		t.Fatalf("expected failure due to unknown error response to HAVESPACE but succeeded")
	}
	if err == nil {
		t.Fatalf("expected error due to unknown error response to HAVESPACE")
	}
	if _, ok = err.(*managesieve.ServerError); !ok {
		t.Fatalf("expected *managesieve.ServerError, got %T (%q)", err,
			err)
	}

	// invalid script size
	_, err = c.HaveSpace("foo", -999999)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok = err.(managesieve.ProtocolError); !ok {
		t.Fatalf("expected managesieve.ProtocolError, got %T (%q)", err,
			err)
	}

	// quota exceeded
	ok, err = c.HaveSpace("foo", 999999)
	if ok {
		t.Fatalf("expected script to exceed quota")
	}
	if err != nil {
		t.Fatalf("failed to determine whether script exceeds quota: %s",
			err)
	}
}

// handling of errors in response to PUTSCRIPT commands
func TestPutScript(t *testing.T) {
	conn := newFakeConn(minimalServer + "BYE\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	// invalid script name
	_, err = c.PutScript("def\u2028ault", validScript)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ProtocolError); !ok {
		t.Fatalf("expected managesieve.ProtocolError, got %T (%q)", err,
			err)
	}

	// EOF during upload
	_, err = c.PutScript("default", validScript)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(*managesieve.ConnClosedError); !ok {
		t.Fatalf("expected *managesieve.ConnClosedError, got %T (%q)",
			err, err)
	}
}

// handling of a literal string with an invalid length
func TestInvalidScriptLength(t *testing.T) {
	conn := newFakeConn(minimalServer + "{-1}\r\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, err = c.GetScript("default")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// missing script in GETSCRIPT response
func TestEmptyGetScript(t *testing.T) {
	conn := newFakeConn(minimalServer + "OK\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, err = c.GetScript("default")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// invalid script list item which is not a string
func TestListScriptsNonString(t *testing.T) {
	conn := newFakeConn(minimalServer + "XXX\nOK\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, _, err = c.ListScripts()
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// script list item with an invalid atom
func TestListScriptsInvalidAtom(t *testing.T) {
	conn := newFakeConn(minimalServer + "\"default\" XXX\nOK\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, _, err = c.ListScripts()
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// script list with multiple active scripts
var multipleActiveServer string = minimalServer + `"default" ACTIVE
"alternative" ACTIVE
"foo"
OK
`

func TestListScriptsMultipleActive(t *testing.T) {
	conn := newFakeConn(multipleActiveServer)
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, active, err := c.ListScripts()
	if err != nil {
		t.Fatalf("failed to get list of scripts: %s", err)
	}
	// although not allowed, the last script marked as active will be
	// returned
	if active != "alternative" {
		t.Fatalf("expected active script \"alternative\", got \"%s\"",
			active)
	}
}

// script list with an empty line
var listScriptsEmptyLineServer string = minimalServer + `"default" ACTIVE
"alternative"

OK
`

func TestListScriptsEmptyLine(t *testing.T) {
	conn := newFakeConn(listScriptsEmptyLineServer)
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, _, err = c.ListScripts()
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// script list item with trailing string
func TestListScriptsTrailingData(t *testing.T) {
	conn := newFakeConn(minimalServer +
		"\"default\" ACTIVE \"alternative\"\nOK\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	_, _, err = c.ListScripts()
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err,
			err)
	}
}

// renaming to an invalid name
func TestRenameInvalidName(t *testing.T) {
	conn := newFakeConn(minimalServer + "NO\n")
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	err = c.RenameScript("default", "a\u2028bc")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ProtocolError); !ok {
		t.Fatalf("expected managesieve.ProtocolError, got %T (%q)",
			err, err)
	}
}

// legacy server which does not implement RFC 5804
var legacyServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
OK
NO
NO
NO
`

func TestLegacyServer(t *testing.T) {
	conn := newFakeConn(legacyServer)
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	err = c.Noop()
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.NotSupportedError); !ok {
		t.Fatalf("expected managesieve.NotSupportedError, got %T (%q)",
			err, err)
	}

	err = c.RenameScript("default", "alternative")
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.NotSupportedError); !ok {
		t.Fatalf("expected managesieve.NotSupportedError, got %T (%q)",
			err, err)
	}

	_, err = c.CheckScript(validScript)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.NotSupportedError); !ok {
		t.Fatalf("expected managesieve.NotSupportedError, got %T (%q)",
			err, err)
	}
}

// PLAIN authentication without TLS on non-localhost server
func TestPlainAuthNoTLS(t *testing.T) {
	conn := newFakeConn(minimalServer)
	c, err := managesieve.NewClient(conn, "imap.example.net")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "imap.example.net")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("error expected due to SASL PLAIN authentication without a TLS connection")
	}
	if err != managesieve.ErrPlainAuthTLSRequired {
		t.Fatalf("expected managesieve.ErrPlainAuthTLSRequired, got %T (%q)", err, err)
	}
}

// mismatch between actual and expected hostname
func TestPlainAuthHostnameMismatch(t *testing.T) {
	conn := newFakeConn(minimalServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "imap.example.net")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(*managesieve.HostNameVerificationError); !ok {
		t.Fatalf("expected *managesieve.HostNameVerificationError, got %T (%q)", err, err)
	}
}

// capabilities sent by the server after negotiating a secure SASL-layer
var authCapabilitiesResponse string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
"cXV1eCxnYXJwbHk="
"d2FsZG8sZnJlZA=="
OK (SASL "eHl6enkgdGh1ZA==")
"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
"default"
OK
`

func TestAuthCapabilitiesResponse(t *testing.T) {
	conn := newFakeConn(authCapabilitiesResponse)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{}
	if err = c.Authenticate(auth); err != nil {
		t.Fatalf("failed to authenticate: %s", err)
	}

	if scripts, _, err := c.ListScripts(); err != nil {
		t.Fatalf("failed to list scripts after authentication: %s", err)
	} else if len(scripts) != 1 {
		t.Fatalf("expected list of scripts but got none")
	}
}

// authentication failure due to invalid credentials
func TestPlainAuthDenied(t *testing.T) {
	conn := newFakeConn(minimalServer +
		"NO \"invalid username or password\"\n")
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	if err = c.Authenticate(auth); err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.AuthenticationError); !ok {
		t.Fatalf("expected managesieve.AuthenticationError, got %T (%q)", err, err)
	}
}

// missing SASL PLAIN authentication method
var plainAuthMissingServer string = `"IMPLEMENTATION" "Example"
"SASL" "TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
`

func TestPlainMissingDenied(t *testing.T) {
	conn := newFakeConn(plainAuthMissingServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but authentication succeeded")
	}
	if err != managesieve.ErrPlainAuthNotSupported {
		t.Fatalf("expected managesieve.ErrPlainAuthNotSupported, got %T (%q)", err, err)
	}
}

// custom SASL authentication method handling with a challenge-response exchange
var customAuthServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
"cXV1eCxnYXJwbHk="
"d2FsZG8sZnJlZA=="
OK (SASL "eHl6enkgdGh1ZA==")
"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
`

var (
	ErrTestAuthNotSupported = errors.New("the server does not support TEST authentication")
	ErrTestAuthFailed       = errors.New("client authentication failed")
)

type testAuth struct {
	WantError bool
	i         int
}

func (a *testAuth) Start(server *managesieve.ServerInfo) (string, []byte, error) {
	if !server.HaveAuth("TEST") {
		return "TEST", nil, ErrTestAuthNotSupported
	}

	return "TEST", []byte("baz,qux"), nil
}

func (a *testAuth) Next(challenge []byte, more bool) ([]byte, error) {
	if a.i == 0 && a.WantError {
		return nil, ErrTestAuthFailed
	}

	var resp []byte
	if !more {
		resp = []byte("plugh,xyzzy")
	} else {
		a.i++
		resp = []byte(fmt.Sprintf("qux=%d", a.i))
	}
	return resp, nil
}

func (a *testAuth) SASLSecurityLayer() bool {
	return true
}

// authentication using a custom authentication method
func TestCustomAuth(t *testing.T) {
	conn := newFakeConn(customAuthServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{}
	if err = c.Authenticate(auth); err != nil {
		t.Fatalf("failed to authenticate using custom authentication method: %s", err)
	}
}

// requesting a non-existent SASL authentication method
func TestNonexistentAuth(t *testing.T) {
	conn := newFakeConn(minimalServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{}
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if err != ErrTestAuthNotSupported {
		t.Fatalf("expected ErrAuthMethodNotSupported, got %T (%q)",
			err, err)
	}
}

// SASL authentication method aborted by the client
var abortAuthServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
"cXV1eCxnYXJwbHk="
NO "aborted by client"
`

// handle error raised by client-side custom authentication handler during
// challenge-response exchange and abort authentication
func TestAbortAuth(t *testing.T) {
	conn := newFakeConn(abortAuthServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{WantError: true}
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.AuthenticationError); !ok {
		t.Fatalf("expected managesieve.AuthenticationError, got %T (%q)", err, err)
	}
}

// custom SASL authentication method handling with a challenge which is not a
// base64-encoded string
var corruptAuthServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
"*****"
`

func TestCustomAuthCorrupt(t *testing.T) {
	conn := newFakeConn(corruptAuthServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{}
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err, err)
	}

}

// response to aborted SASL authentication is not NO
var invalidAuthAbortServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
"cXV1eCxnYXJwbHk="
OK
`

func TestInvalidAuthAbort(t *testing.T) {
	conn := newFakeConn(invalidAuthAbortServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{WantError: true}
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ProtocolError); !ok {
		t.Fatalf("expected managesieve.ProtocolError, got %T (%q)", err, err)
	}
}

// invalid response to SASL authentication attempt
var invalidAuthServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
XXX
`

func TestInvalidAuthResponse(t *testing.T) {
	conn := newFakeConn(invalidAuthServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err, err)
	}
}

// invalid trailing argument after first SASL response code argument
var trailingSASLResponseArgServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
OK (SASL "Zm9v" XXX)
`

func TestTrailingSASLResponseArg(t *testing.T) {
	conn := newFakeConn(trailingSASLResponseArgServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err, err)
	}
}

// trailing argument after first SASL response code argument
var trailingSASLResonseArgServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
OK (SASL "Zm9v" "bar")
`

func TestTrailingSASLResonseArg(t *testing.T) {
	conn := newFakeConn(trailingSASLResonseArgServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err, err)
	}
}

// SASL response code argument is not base64-encoded
var invalidSASLResponseArgServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
OK (SASL "*****")
`

func TestInvalidSASLResponseArg(t *testing.T) {
	conn := newFakeConn(invalidSASLResponseArgServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.ParserError); !ok {
		t.Fatalf("expected managesieve.ParserError, got %T (%q)", err, err)
	}
}

// argument passed with SASL response code rejected by client authentication
// handler
var saslResponseRejectedServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN TEST"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
OK (SASL "Zm9v")
`

func TestSASLResponseRejected(t *testing.T) {
	conn := newFakeConn(saslResponseRejectedServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := &testAuth{WantError: true}
	if err = c.Authenticate(auth); err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(managesieve.AuthenticationError); !ok {
		t.Fatalf("expected managesieve.AuthenticationError, got %T (%q)", err, err)
	}
}

// CAPABILITIES command after authentication failed
var authCapabilitiesFailedServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
OK
NO
`

func TestAuthCapabilitiesFailed(t *testing.T) {
	conn := newFakeConn(authCapabilitiesFailedServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(*managesieve.ServerError); !ok {
		t.Fatalf("expected *managesieve.ServerError, got %T (%q)", err, err)
	}
}

// BYE in response to SASL authentication attempt
var authByeServer string = `"IMPLEMENTATION" "Example"
"SASL" "PLAIN"
"SIEVE" "fileinto vacation"
"StARTTLS"
"VERSION" "1.0"
OK
BYE "authentication denied"
`

func TestAuthBye(t *testing.T) {
	conn := newFakeConn(authByeServer)
	c, err := managesieve.NewClient(conn, "localhost")
	if err != nil {
		t.Fatalf("failed to create client: %s", err)
	}

	auth := managesieve.PlainAuth("", "foo", "S3cR3T", "localhost")
	err = c.Authenticate(auth)
	if err == nil {
		t.Fatalf("expected error but succeeded")
	}
	if _, ok := err.(*managesieve.ConnClosedError); !ok {
		t.Fatalf("expected *managesieve.ConnClosedError, got %T (%q)", err, err)
	}
}