diff --git a/.build.yml b/.build.yml
new file mode 100644
index 0000000..daa6006
--- /dev/null
+++ b/.build.yml
@@ -0,0 +1,19 @@
+image: alpine/latest
+packages:
+  - go
+  # Required by codecov
+  - bash
+  - findutils
+sources:
+  - https://github.com/emersion/go-sasl
+tasks:
+  - build: |
+      cd go-sasl
+      go build -v ./...
+  - test: |
+      cd go-sasl
+      go test -coverprofile=coverage.txt -covermode=atomic ./...
+  - upload-coverage: |
+      cd go-sasl
+      export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1
+      curl -s https://codecov.io/bash | bash
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 92823df..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-language: go
-go:
-  - 1.5
diff --git a/README.md b/README.md
index 70d9aed..6bd47ba 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,17 @@
 # go-sasl
 
-[![GoDoc](https://godoc.org/github.com/emersion/go-sasl?status.svg)](https://godoc.org/github.com/emersion/go-sasl)
+[![godocs.io](https://godocs.io/github.com/emersion/go-sasl?status.svg)](https://godocs.io/github.com/emersion/go-sasl)
 [![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl)
 
 A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
 
 Implemented mechanisms:
+
 * [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
 * [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
 * [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)
 * [PLAIN](https://tools.ietf.org/html/rfc4616)
 * [OAUTHBEARER](https://tools.ietf.org/html/rfc7628)
-* [XOAUTH2](https://developers.google.com/gmail/xoauth2_protocol) (non-standard, use OAUTHBEARER instead)
 
 ## License
 
diff --git a/anonymous.go b/anonymous.go
index 8ccb817..abcb753 100644
--- a/anonymous.go
+++ b/anonymous.go
@@ -27,7 +27,7 @@ func NewAnonymousClient(trace string) Client {
 type AnonymousAuthenticator func(trace string) error
 
 type anonymousServer struct {
-	done bool
+	done         bool
 	authenticate AnonymousAuthenticator
 }
 
diff --git a/debian/changelog b/debian/changelog
index ac1a49f..5894040 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,10 @@
-golang-github-emersion-go-sasl (0.0~git20191210.430746e-3) UNRELEASED; urgency=low
+golang-github-emersion-go-sasl (0.0~git20211008.0b9dcfb-1) UNRELEASED; urgency=low
 
   * Trim trailing whitespace.
   * Set upstream metadata fields: Bug-Database, Bug-Submit.
+  * New upstream snapshot.
 
- -- Debian Janitor <janitor@jelmer.uk>  Thu, 03 Sep 2020 19:51:58 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 20 Mar 2022 07:21:11 -0000
 
 golang-github-emersion-go-sasl (0.0~git20191210.430746e-2) unstable; urgency=medium
 
diff --git a/external.go b/external.go
index da070c8..6572351 100644
--- a/external.go
+++ b/external.go
@@ -1,5 +1,10 @@
 package sasl
 
+import (
+	"bytes"
+	"errors"
+)
+
 // The EXTERNAL mechanism name.
 const External = "EXTERNAL"
 
@@ -24,3 +29,39 @@ func (a *externalClient) Next(challenge []byte) (response []byte, err error) {
 func NewExternalClient(identity string) Client {
 	return &externalClient{identity}
 }
+
+// ExternalAuthenticator authenticates users with the EXTERNAL mechanism. If
+// the identity is left blank, it indicates that it is the same as the one used
+// in the external credentials. If identity is not empty and the server doesn't
+// support it, an error must be returned.
+type ExternalAuthenticator func(identity string) error
+
+type externalServer struct {
+	done         bool
+	authenticate ExternalAuthenticator
+}
+
+func (a *externalServer) Next(response []byte) (challenge []byte, done bool, err error) {
+	if a.done {
+		return nil, false, ErrUnexpectedClientResponse
+	}
+
+	// No initial response, send an empty challenge
+	if response == nil {
+		return []byte{}, false, nil
+	}
+
+	a.done = true
+
+	if bytes.Contains(response, []byte("\x00")) {
+		return nil, false, errors.New("identity contains a NUL character")
+	}
+
+	return nil, true, a.authenticate(string(response))
+}
+
+// NewExternalServer creates a server implementation of the EXTERNAL
+// authentication mechanism, as described in RFC 4422.
+func NewExternalServer(authenticator ExternalAuthenticator) Server {
+	return &externalServer{authenticate: authenticator}
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..dc3c9a4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/emersion/go-sasl
+
+go 1.12
diff --git a/oauthbearer.go b/oauthbearer.go
index 463c337..a0639b1 100644
--- a/oauthbearer.go
+++ b/oauthbearer.go
@@ -1,9 +1,12 @@
 package sasl
 
 import (
+	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"strconv"
+	"strings"
 )
 
 // The OAUTHBEARER mechanism name.
@@ -61,3 +64,128 @@ func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) {
 func NewOAuthBearerClient(opt *OAuthBearerOptions) Client {
 	return &oauthBearerClient{*opt}
 }
+
+type OAuthBearerAuthenticator func(opts OAuthBearerOptions) *OAuthBearerError
+
+type oauthBearerServer struct {
+	done         bool
+	failErr      error
+	authenticate OAuthBearerAuthenticator
+}
+
+func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) {
+	blob, err := json.Marshal(OAuthBearerError{
+		Status:  "invalid_request",
+		Schemes: "bearer",
+	})
+	if err != nil {
+		panic(err) // wtf
+	}
+	a.failErr = errors.New(descr)
+	return blob, false, nil
+}
+
+func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool, err error) {
+	// Per RFC, we cannot just send an error, we need to return JSON-structured
+	// value as a challenge and then after getting dummy response from the
+	// client stop the exchange.
+	if a.failErr != nil {
+		// Server libraries (go-smtp, go-imap) will not call Next on
+		// protocol-specific SASL cancel response ('*'). However, GS2 (and
+		// indirectly OAUTHBEARER) defines a protocol-independent way to do so
+		// using 0x01.
+		if len(response) != 1 && response[0] != 0x01 {
+			return nil, true, errors.New("unexpected response")
+		}
+		return nil, true, a.failErr
+	}
+
+	if a.done {
+		err = ErrUnexpectedClientResponse
+		return
+	}
+
+	// Generate empty challenge.
+	if response == nil {
+		return []byte{}, false, nil
+	}
+
+	a.done = true
+
+	// Cut n,a=username,\x01host=...\x01auth=...
+	// into
+	//   n
+	//   a=username
+	//   \x01host=...\x01auth=...\x01\x01
+	parts := bytes.SplitN(response, []byte{','}, 3)
+	if len(parts) != 3 {
+		return a.fail("Invalid response")
+	}
+	if !bytes.Equal(parts[0], []byte{'n'}) {
+		return a.fail("Invalid response, missing 'n'")
+	}
+	opts := OAuthBearerOptions{}
+	if !bytes.HasPrefix(parts[1], []byte("a=")) {
+		return a.fail("Invalid response, missing 'a'")
+	}
+	opts.Username = string(bytes.TrimPrefix(parts[1], []byte("a=")))
+
+	// Cut \x01host=...\x01auth=...\x01\x01
+	// into
+	//   *empty*
+	//   host=...
+	//   auth=...
+	//   *empty*
+	//
+	// Note that this code does not do a lot of checks to make sure the input
+	// follows the exact format specified by RFC.
+	params := bytes.Split(parts[2], []byte{0x01})
+	for _, p := range params {
+		// Skip empty fields (one at start and end).
+		if len(p) == 0 {
+			continue
+		}
+
+		pParts := bytes.SplitN(p, []byte{'='}, 2)
+		if len(pParts) != 2 {
+			return a.fail("Invalid response, missing '='")
+		}
+
+		switch string(pParts[0]) {
+		case "host":
+			opts.Host = string(pParts[1])
+		case "port":
+			port, err := strconv.ParseUint(string(pParts[1]), 10, 16)
+			if err != nil {
+				return a.fail("Invalid response, malformed 'port' value")
+			}
+			opts.Port = int(port)
+		case "auth":
+			const prefix = "bearer "
+			strValue := string(pParts[1])
+			// Token type is case-insensitive.
+			if !strings.HasPrefix(strings.ToLower(strValue), prefix) {
+				return a.fail("Unsupported token type")
+			}
+			opts.Token = strValue[len(prefix):]
+		default:
+			return a.fail("Invalid response, unknown parameter: " + string(pParts[0]))
+		}
+	}
+
+	authzErr := a.authenticate(opts)
+	if authzErr != nil {
+		blob, err := json.Marshal(authzErr)
+		if err != nil {
+			panic(err) // wtf
+		}
+		a.failErr = authzErr
+		return blob, false, nil
+	}
+
+	return nil, true, nil
+}
+
+func NewOAuthBearerServer(auth OAuthBearerAuthenticator) Server {
+	return &oauthBearerServer{authenticate: auth}
+}
diff --git a/oauthbearer_test.go b/oauthbearer_test.go
index 5603150..f98c86b 100644
--- a/oauthbearer_test.go
+++ b/oauthbearer_test.go
@@ -75,3 +75,70 @@ func TestNewOAuthBearerClient(t *testing.T) {
 	}
 
 }
+
+func TestOAuthBearerServerAndClient(t *testing.T) {
+	oauthErr := sasl.OAuthBearerError{
+		Status:  "invalid_token",
+		Scope:   "email",
+		Schemes: "bearer",
+	}
+	authenticator := func(opts sasl.OAuthBearerOptions) *sasl.OAuthBearerError {
+		if opts.Username == "fxcp" && opts.Token == "VkIvciKi9ijpiKNWrQmYCJrzgd9QYCMB" {
+			return nil
+		}
+		return &oauthErr
+	}
+
+	t.Run("valid token", func(t *testing.T) {
+		s := sasl.NewOAuthBearerServer(authenticator)
+		c := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
+			Username: "fxcp",
+			Token:    "VkIvciKi9ijpiKNWrQmYCJrzgd9QYCMB",
+		})
+		_, ir, err := c.Start()
+		if err != nil {
+			t.Fatal(err)
+		}
+		_, done, err := s.Next(ir)
+		if err != nil {
+			t.Fatal("Unexpected error")
+		}
+		if !done {
+			t.Fatal("Exchange is not complete")
+		}
+	})
+
+	t.Run("invalid token", func(t *testing.T) {
+		s := sasl.NewOAuthBearerServer(authenticator)
+		c := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
+			Username: "fxcp",
+			Token:    "adiffrentone",
+		})
+		_, ir, err := c.Start()
+		if err != nil {
+			t.Fatal(err)
+		}
+		val, done, err := s.Next(ir)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if done {
+			t.Fatal("Exchange is marked complete")
+		}
+
+		_, err = c.Next(val)
+		if err == nil {
+			t.Fatal("Expected an error")
+		}
+		authzErr, ok := err.(*sasl.OAuthBearerError)
+		if !ok {
+			t.Fatal("Not OAuthBearerError")
+		}
+		if authzErr.Status != "invalid_token" {
+			t.Fatal("Wrong status:", authzErr.Status)
+		}
+		if authzErr.Scope != "email" {
+			t.Fatal("Wrong scope:", authzErr.Scope)
+		}
+	})
+}
diff --git a/plain.go b/plain.go
index 344ed17..419c952 100644
--- a/plain.go
+++ b/plain.go
@@ -38,7 +38,7 @@ func NewPlainClient(identity, username, password string) Client {
 type PlainAuthenticator func(identity, username, password string) error
 
 type plainServer struct {
-	done bool
+	done         bool
 	authenticate PlainAuthenticator
 }
 
diff --git a/plain_test.go b/plain_test.go
index d720cc1..182446c 100644
--- a/plain_test.go
+++ b/plain_test.go
@@ -27,7 +27,7 @@ func TestNewPlainClient(t *testing.T) {
 
 func TestNewPlainServer(t *testing.T) {
 	var authenticated = false
-	s := sasl.NewPlainServer(func (identity, username, password string) error {
+	s := sasl.NewPlainServer(func(identity, username, password string) error {
 		if username != "username" {
 			return errors.New("Invalid username: " + username)
 		}
diff --git a/sasl.go b/sasl.go
index c209144..525da88 100644
--- a/sasl.go
+++ b/sasl.go
@@ -12,7 +12,7 @@ import (
 
 // Common SASL errors.
 var (
-	ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
+	ErrUnexpectedClientResponse  = errors.New("sasl: unexpected client response")
 	ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge")
 )
 
diff --git a/xoauth2.go b/xoauth2.go
deleted file mode 100644
index 9e5d03e..0000000
--- a/xoauth2.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package sasl
-
-import (
-	"encoding/json"
-	"fmt"
-)
-
-// The XOAUTH2 mechanism name.
-const Xoauth2 = "XOAUTH2"
-
-// An XOAUTH2 error.
-type Xoauth2Error struct {
-	Status string `json:"status"`
-	Schemes string `json:"schemes"`
-	Scope string `json:"scope"`
-}
-
-// Implements error.
-func (err *Xoauth2Error) Error() string {
-	return fmt.Sprintf("XOAUTH2 authentication error (%v)", err.Status)
-}
-
-type xoauth2Client struct {
-	Username string
-	Token string
-}
-
-func (a *xoauth2Client) Start() (mech string, ir []byte, err error) {
-	mech = Xoauth2
-	ir = []byte("user=" + a.Username + "\x01auth=Bearer " + a.Token + "\x01\x01")
-	return
-}
-
-func (a *xoauth2Client) Next(challenge []byte) ([]byte, error) {
-	// Server sent an error response
-	xoauth2Err := &Xoauth2Error{}
-	if err := json.Unmarshal(challenge, xoauth2Err); err != nil {
-		return nil, err
-	} else {
-		return nil, xoauth2Err
-	}
-}
-
-// An implementation of the XOAUTH2 authentication mechanism, as
-// described in https://developers.google.com/gmail/xoauth2_protocol.
-func NewXoauth2Client(username, token string) Client {
-	return &xoauth2Client{username, token}
-}