Mercurial > projects > managesieve
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) + } +}