diff --git a/dovecot/dict.go b/dovecot/dict.go
index 50835edd28283dac6ee062b25551095e98ddb6d1..320b9e316d7929966f2321cac49d7d4605b64ee7 100644
--- a/dovecot/dict.go
+++ b/dovecot/dict.go
@@ -17,7 +17,10 @@ var (
 	noMatchResponse = []byte{'N', '\n'}
 )
 
-const supportedDictProtocolVersion = 2
+const (
+	supportedDictProtocolVersionMin = 2
+	supportedDictProtocolVersionMax = 3
+)
 
 // DictDatabase is an interface to a key/value store by way of the Lookup
 // method.
@@ -29,7 +32,7 @@ type DictDatabase interface {
 }
 
 // DictProxyServer exposes a Database using the Dovecot dict proxy
-// protocol (see https://wiki2.dovecot.org/AuthDatabase/Dict).
+// protocol (see https://doc.dovecot.org/developer_manual/design/dict_protocol/).
 //
 // It implements the unix.LineHandler interface from the
 // ai3/go-common/unix package.
@@ -59,13 +62,13 @@ func (p *DictProxyServer) ServeLine(ctx context.Context, lw unix.LineResponseWri
 }
 
 func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error {
-	fields := bytes.Split(arg, []byte{'\t'})
+	fields := splitFields(arg)
 	if len(fields) < 1 {
 		return errors.New("could not parse HELLO")
 	}
 
 	majorVersion, _ := strconv.Atoi(string(fields[0]))
-	if majorVersion != supportedDictProtocolVersion {
+	if majorVersion < supportedDictProtocolVersionMin || majorVersion > supportedDictProtocolVersionMax {
 		return fmt.Errorf("unsupported protocol version %d", majorVersion)
 	}
 
@@ -73,7 +76,15 @@ func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseW
 }
 
 func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error {
-	obj, ok, err := p.db.Lookup(ctx, string(arg))
+	// Support protocol versions 2 and 3 by looking for the \t
+	// field separator, which should not appear in the key in
+	// version 2 anyway.
+	fields := splitFields(arg)
+	if len(fields) < 1 {
+		return errors.New("could not parse LOOKUP")
+	}
+
+	obj, ok, err := p.db.Lookup(ctx, string(fields[0]))
 	if err != nil {
 		log.Printf("error: %v", err)
 		return lw.WriteLine(failResponse)
@@ -91,3 +102,41 @@ func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponse
 	//buf.Write([]byte{'\n'})
 	return lw.WriteLine(buf.Bytes())
 }
+
+var dovecotEscapeChars = map[byte]byte{
+	'0': 0,
+	'1': 1,
+	't': '\t',
+	'r': '\r',
+	'l': '\n',
+}
+
+var fieldSeparator = []byte{'\t'}
+
+func unescapeInplace(b []byte) []byte {
+	var esc bool
+	var j int
+	for i := 0; i < len(b); i++ {
+		c := b[i]
+		if esc {
+			if escC, ok := dovecotEscapeChars[c]; ok {
+				c = escC
+			}
+			esc = false
+		} else if c == '\001' {
+			esc = true
+			continue
+		}
+		b[j] = c
+		j++
+	}
+	return b[:j]
+}
+
+func splitFields(b []byte) [][]byte {
+	fields := bytes.Split(b, fieldSeparator)
+	for i := 0; i < len(fields); i++ {
+		fields[i] = unescapeInplace(fields[i])
+	}
+	return fields
+}
diff --git a/dovecot/dict_test.go b/dovecot/dict_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e9800ae4f6344f8774c2136c4890474d16f73f94
--- /dev/null
+++ b/dovecot/dict_test.go
@@ -0,0 +1,23 @@
+package dovecot
+
+import (
+	"testing"
+)
+
+func TestUnescape(t *testing.T) {
+	for _, td := range []struct {
+		input, exp string
+	}{
+		{"boo", "boo"},
+		{"bo\001t", "bo\t"},
+		{"bo\001t\001l", "bo\t\n"},
+		{"bo\001t\0011", "bo\t\001"},
+	} {
+		out := make([]byte, len(td.input))
+		copy(out, td.input)
+		out = unescapeInplace(out)
+		if string(out) != td.exp {
+			t.Errorf("unescape('%s') returned '%s', expected '%s'", td.input, out, td.exp)
+		}
+	}
+}