diff managesieve_test.go @ 0:6369453d47a3

Initial revision
author Guido Berhoerster <guido+managesieve@berhoerster.name>
date Thu, 15 Oct 2020 09:11:05 +0200
parents
children f9bb517e9447
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/managesieve_test.go	Thu Oct 15 09:11:05 2020 +0200
@@ -0,0 +1,1189 @@
+// 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";
+}
+`
+
+// 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) "line 8: server redirect action limit is 2, this redirect might be ignored"
+OK
+OK (WARNINGS) "line 8: server redirect action limit is 2, this redirect might be ignored"
+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 err = c.CheckScript(validScript); err != nil {
+		t.Fatalf("CHECKSCRIPT failed: %s", err)
+	}
+
+	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 err = c.PutScript("default", validScript); err != nil {
+		t.Fatalf("PUTSCRIPT failed: %s", err)
+	}
+
+	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)
+	}
+}
+
+// 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
+}
+
+// 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
+// bas64-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)
+	}
+}