Repository: emersion/go-imap
Pull request #563 (2023-12-06 13:19:35)

imapclient: Close returned err.NetClosed after Logout
server closes connection after Logout and Close should not return error NetClosed when called after logout 

emersion: Can we avoid using `%w` everywhere? That makes the underlying errors part of our API.

Patch:
diff --git a/.build.yml b/.build.yml
index c055962..fea091c 100644
--- a/.build.yml
+++ b/.build.yml
@@ -10,3 +10,6 @@ tasks:
   - test: |
       cd go-imap
       go test -race ./...
+  - gofmt: |
+      cd go-imap
+      test -z $(gofmt -l .)
diff --git a/append.go b/append.go
index 4348e62..13d887f 100644
--- a/append.go
+++ b/append.go
@@ -12,5 +12,7 @@ type AppendOptions struct {
 
 // AppendData is the data returned by an APPEND command.
 type AppendData struct {
-	UID, UIDValidity uint32 // requires UIDPLUS or IMAP4rev2
+	// requires UIDPLUS or IMAP4rev2
+	UID         UID
+	UIDValidity uint32
 }
diff --git a/copy.go b/copy.go
index 1988a6a..c8430d7 100644
--- a/copy.go
+++ b/copy.go
@@ -4,6 +4,6 @@ package imap
 type CopyData struct {
 	// requires UIDPLUS or IMAP4rev2
 	UIDValidity uint32
-	SourceUIDs  SeqSet
-	DestUIDs    SeqSet
+	SourceUIDs  NumSet
+	DestUIDs    NumSet
 }
diff --git a/imap.go b/imap.go
index 6f5c5eb..7b43357 100644
--- a/imap.go
+++ b/imap.go
@@ -100,3 +100,6 @@ type LiteralReader interface {
 	io.Reader
 	Size() int64
 }
+
+// UID is a message unique identifier.
+type UID uint32
diff --git a/imapclient/client.go b/imapclient/client.go
index 1f8dbad..7d15fcd 100644
--- a/imapclient/client.go
+++ b/imapclient/client.go
@@ -22,6 +22,7 @@ package imapclient
 import (
 	"bufio"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"io"
 	"mime"
@@ -317,12 +318,12 @@ func (c *Client) Close() error {
 	c.mutex.Unlock()
 
 	// Ignore net.ErrClosed here, because we also call conn.Close in c.read
-	if err := c.conn.Close(); err != nil && err != net.ErrClosed {
+	if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
 		return err
 	}
 
 	<-c.decCh
-	if err := c.decErr; err != nil {
+	if err := c.decErr; err != nil && !errors.Is(err, net.ErrClosed) {
 		return err
 	}
 
@@ -638,8 +639,11 @@ func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand,
 			}
 			c.setCaps(caps)
 		case "APPENDUID":
-			var uidValidity, uid uint32
-			if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uid) {
+			var (
+				uidValidity uint32
+				uid         imap.UID
+			)
+			if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectUID(&uid) {
 				return nil, fmt.Errorf("in resp-code-apnd: %v", c.dec.Err())
 			}
 			if cmd, ok := cmd.(*AppendCommand); ok {
@@ -760,8 +764,8 @@ func (c *Client) readResponseData(typ string) error {
 					handler(&UnilateralDataMailbox{PermanentFlags: flags})
 				}
 			case "UIDNEXT":
-				var uidNext uint32
-				if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidNext) {
+				var uidNext imap.UID
+				if !c.dec.ExpectSP() || !c.dec.ExpectUID(&uidNext) {
 					return c.dec.Err()
 				}
 				if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil {
diff --git a/imapclient/copy.go b/imapclient/copy.go
index 7addae3..73422a7 100644
--- a/imapclient/copy.go
+++ b/imapclient/copy.go
@@ -5,23 +5,23 @@ import (
 	"github.com/emersion/go-imap/v2/internal/imapwire"
 )
 
-func (c *Client) copy(uid bool, seqSet imap.SeqSet, mailbox string) *CopyCommand {
+func (c *Client) copy(uid bool, seqSet imap.NumSet, mailbox string) *CopyCommand {
 	cmd := &CopyCommand{}
 	enc := c.beginCommand(uidCmdName("COPY", uid), cmd)
-	enc.SP().SeqSet(seqSet).SP().Mailbox(mailbox)
+	enc.SP().NumSet(seqSet).SP().Mailbox(mailbox)
 	enc.end()
 	return cmd
 }
 
 // Copy sends a COPY command.
-func (c *Client) Copy(seqSet imap.SeqSet, mailbox string) *CopyCommand {
+func (c *Client) Copy(seqSet imap.NumSet, mailbox string) *CopyCommand {
 	return c.copy(false, seqSet, mailbox)
 }
 
 // UIDCopy sends a UID COPY command.
 //
 // See Copy.
-func (c *Client) UIDCopy(seqSet imap.SeqSet, mailbox string) *CopyCommand {
+func (c *Client) UIDCopy(seqSet imap.NumSet, mailbox string) *CopyCommand {
 	return c.copy(true, seqSet, mailbox)
 }
 
@@ -35,9 +35,9 @@ func (cmd *CopyCommand) Wait() (*imap.CopyData, error) {
 	return &cmd.data, cmd.cmd.Wait()
 }
 
-func readRespCodeCopy(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.SeqSet, err error) {
-	if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectSeqSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectSeqSet(&dstUIDs) {
-		return 0, imap.SeqSet{}, imap.SeqSet{}, dec.Err()
+func readRespCodeCopy(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.NumSet, err error) {
+	if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectNumSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectNumSet(&dstUIDs) {
+		return 0, imap.NumSet{}, imap.NumSet{}, dec.Err()
 	}
 	return uidValidity, srcUIDs, dstUIDs, nil
 }
diff --git a/imapclient/expunge.go b/imapclient/expunge.go
index e44bff2..50bcf71 100644
--- a/imapclient/expunge.go
+++ b/imapclient/expunge.go
@@ -14,10 +14,10 @@ func (c *Client) Expunge() *ExpungeCommand {
 // UIDExpunge sends a UID EXPUNGE command.
 //
 // This command requires support for IMAP4rev2 or the UIDPLUS extension.
-func (c *Client) UIDExpunge(uids imap.SeqSet) *ExpungeCommand {
+func (c *Client) UIDExpunge(uids imap.NumSet) *ExpungeCommand {
 	cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
 	enc := c.beginCommand("UID EXPUNGE", cmd)
-	enc.SP().SeqSet(uids)
+	enc.SP().NumSet(uids)
 	enc.end()
 	return cmd
 }
diff --git a/imapclient/fetch.go b/imapclient/fetch.go
index 35336ba..419e1e0 100644
--- a/imapclient/fetch.go
+++ b/imapclient/fetch.go
@@ -12,7 +12,7 @@ import (
 	"github.com/emersion/go-imap/v2/internal/imapwire"
 )
 
-func (c *Client) fetch(uid bool, seqSet imap.SeqSet, options *imap.FetchOptions) *FetchCommand {
+func (c *Client) fetch(uid bool, seqSet imap.NumSet, options *imap.FetchOptions) *FetchCommand {
 	if options == nil {
 		options = new(imap.FetchOptions)
 	}
@@ -23,7 +23,7 @@ func (c *Client) fetch(uid bool, seqSet imap.SeqSet, options *imap.FetchOptions)
 		msgs:   make(chan *FetchMessageData, 128),
 	}
 	enc := c.beginCommand(uidCmdName("FETCH", uid), cmd)
-	enc.SP().SeqSet(seqSet).SP()
+	enc.SP().NumSet(seqSet).SP()
 	writeFetchItems(enc.Encoder, uid, options)
 	if options.ChangedSince != 0 {
 		enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince).Special(')')
@@ -38,14 +38,14 @@ func (c *Client) fetch(uid bool, seqSet imap.SeqSet, options *imap.FetchOptions)
 // defer a call to FetchCommand.Close.
 //
 // A nil options pointer is equivalent to a zero options value.
-func (c *Client) Fetch(seqSet imap.SeqSet, options *imap.FetchOptions) *FetchCommand {
+func (c *Client) Fetch(seqSet imap.NumSet, options *imap.FetchOptions) *FetchCommand {
 	return c.fetch(false, seqSet, options)
 }
 
 // UIDFetch sends a UID FETCH command.
 //
 // See Fetch.
-func (c *Client) UIDFetch(seqSet imap.SeqSet, options *imap.FetchOptions) *FetchCommand {
+func (c *Client) UIDFetch(seqSet imap.NumSet, options *imap.FetchOptions) *FetchCommand {
 	return c.fetch(true, seqSet, options)
 }
 
@@ -160,8 +160,8 @@ type FetchCommand struct {
 	cmd
 
 	uid        bool
-	seqSet     imap.SeqSet
-	recvSeqSet imap.SeqSet
+	seqSet     imap.NumSet
+	recvNumSet imap.NumSet
 
 	msgs chan *FetchMessageData
 	prev *FetchMessageData
@@ -175,7 +175,8 @@ func (cmd *FetchCommand) Next() *FetchMessageData {
 	if cmd.prev != nil {
 		cmd.prev.discard()
 	}
-	return <-cmd.msgs
+	cmd.prev = <-cmd.msgs
+	return cmd.prev
 }
 
 // Close releases the command.
@@ -346,7 +347,7 @@ func (FetchItemDataRFC822Size) fetchItemData() {}
 
 // FetchItemDataUID holds data returned by FETCH UID.
 type FetchItemDataUID struct {
-	UID uint32
+	UID imap.UID
 }
 
 func (FetchItemDataUID) fetchItemData() {}
@@ -386,7 +387,7 @@ type FetchMessageBuffer struct {
 	Envelope          *imap.Envelope
 	InternalDate      time.Time
 	RFC822Size        int64
-	UID               uint32
+	UID               imap.UID
 	BodyStructure     imap.BodyStructure
 	BodySection       map[*imap.FetchItemBodySection][]byte
 	BinarySection     map[*imap.FetchItemBinarySection][]byte
@@ -449,7 +450,7 @@ func (c *Client) handleFetch(seqNum uint32) error {
 	// the response data. But the response data comes in in a streaming
 	// fashion: it can contain literals. Assume that the UID will be returned
 	// before any literal.
-	var uid uint32
+	var uid imap.UID
 	handled := false
 	handleMsg := func() {
 		if handled {
@@ -465,14 +466,14 @@ func (c *Client) handleFetch(seqNum uint32) error {
 			// Skip if we haven't requested or already handled this message
 			var num uint32
 			if cmd.uid {
-				num = uid
+				num = uint32(uid)
 			} else {
 				num = seqNum
 			}
-			if num == 0 || !cmd.seqSet.Contains(num) || cmd.recvSeqSet.Contains(num) {
+			if num == 0 || !cmd.seqSet.Contains(num) || cmd.recvNumSet.Contains(num) {
 				return false
 			}
-			cmd.recvSeqSet.AddNum(num)
+			cmd.recvNumSet.AddNum(num)
 
 			return true
 		})
@@ -543,7 +544,7 @@ func (c *Client) handleFetch(seqNum uint32) error {
 
 			item = FetchItemDataRFC822Size{Size: size}
 		case "UID":
-			if !dec.ExpectSP() || !dec.ExpectNumber(&uid) {
+			if !dec.ExpectSP() || !dec.ExpectUID(&uid) {
 				return dec.Err()
 			}
 
diff --git a/imapclient/move.go b/imapclient/move.go
index de17dc0..446a94a 100644
--- a/imapclient/move.go
+++ b/imapclient/move.go
@@ -4,7 +4,7 @@ import (
 	"github.com/emersion/go-imap/v2"
 )
 
-func (c *Client) move(uid bool, seqSet imap.SeqSet, mailbox string) *MoveCommand {
+func (c *Client) move(uid bool, seqSet imap.NumSet, mailbox string) *MoveCommand {
 	// If the server doesn't support MOVE, fallback to [UID] COPY,
 	// [UID] STORE +FLAGS.SILENT \Deleted and [UID] EXPUNGE
 	cmdName := "MOVE"
@@ -14,7 +14,7 @@ func (c *Client) move(uid bool, seqSet imap.SeqSet, mailbox string) *MoveCommand
 
 	cmd := &MoveCommand{}
 	enc := c.beginCommand(uidCmdName(cmdName, uid), cmd)
-	enc.SP().SeqSet(seqSet).SP().Mailbox(mailbox)
+	enc.SP().NumSet(seqSet).SP().Mailbox(mailbox)
 	enc.end()
 
 	if cmdName == "COPY" {
@@ -37,14 +37,14 @@ func (c *Client) move(uid bool, seqSet imap.SeqSet, mailbox string) *MoveCommand
 //
 // If the server doesn't support IMAP4rev2 nor the MOVE extension, a fallback
 // with COPY + STORE + EXPUNGE commands is used.
-func (c *Client) Move(seqSet imap.SeqSet, mailbox string) *MoveCommand {
+func (c *Client) Move(seqSet imap.NumSet, mailbox string) *MoveCommand {
 	return c.move(false, seqSet, mailbox)
 }
 
 // UIDMove sends a UID MOVE command.
 //
 // See Move.
-func (c *Client) UIDMove(seqSet imap.SeqSet, mailbox string) *MoveCommand {
+func (c *Client) UIDMove(seqSet imap.NumSet, mailbox string) *MoveCommand {
 	return c.move(true, seqSet, mailbox)
 }
 
@@ -79,6 +79,6 @@ func (cmd *MoveCommand) Wait() (*MoveData, error) {
 type MoveData struct {
 	// requires UIDPLUS or IMAP4rev2
 	UIDValidity uint32
-	SourceUIDs  imap.SeqSet
-	DestUIDs    imap.SeqSet
+	SourceUIDs  imap.NumSet
+	DestUIDs    imap.NumSet
 }
diff --git a/imapclient/search.go b/imapclient/search.go
index 1aafac4..8c509ea 100644
--- a/imapclient/search.go
+++ b/imapclient/search.go
@@ -154,10 +154,10 @@ func writeSearchKey(enc *imapwire.Encoder, criteria *imap.SearchCriteria) {
 	}
 
 	for _, seqSet := range criteria.SeqNum {
-		encodeItem().SeqSet(seqSet)
+		encodeItem().NumSet(seqSet)
 	}
 	for _, seqSet := range criteria.UID {
-		encodeItem().Atom("UID").SP().SeqSet(seqSet)
+		encodeItem().Atom("UID").SP().NumSet(seqSet)
 	}
 
 	if !criteria.Since.IsZero() && !criteria.Before.IsZero() && criteria.Before.Sub(criteria.Since) == 24*time.Hour {
@@ -298,7 +298,7 @@ func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchDa
 			}
 			data.Max = num
 		case "ALL":
-			if !dec.ExpectSeqSet(&data.All) {
+			if !dec.ExpectNumSet(&data.All) {
 				return "", nil, dec.Err()
 			}
 		case "COUNT":
diff --git a/imapclient/status.go b/imapclient/status.go
index 57242b0..d276384 100644
--- a/imapclient/status.go
+++ b/imapclient/status.go
@@ -111,7 +111,9 @@ func readStatusAttVal(dec *imapwire.Decoder, data *imap.StatusData) error {
 		ok = dec.ExpectNumber(&num)
 		data.NumMessages = &num
 	case "UIDNEXT":
-		ok = dec.ExpectNumber(&data.UIDNext)
+		var uidNext imap.UID
+		ok = dec.ExpectUID(&uidNext)
+		data.UIDNext = uidNext
 	case "UIDVALIDITY":
 		ok = dec.ExpectNumber(&data.UIDValidity)
 	case "UNSEEN":
diff --git a/imapclient/store.go b/imapclient/store.go
index 0ad17d2..cc49dc5 100644
--- a/imapclient/store.go
+++ b/imapclient/store.go
@@ -6,10 +6,10 @@ import (
 	"github.com/emersion/go-imap/v2"
 )
 
-func (c *Client) store(uid bool, seqSet imap.SeqSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
+func (c *Client) store(uid bool, seqSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
 	cmd := &FetchCommand{msgs: make(chan *FetchMessageData, 128)}
 	enc := c.beginCommand(uidCmdName("STORE", uid), cmd)
-	enc.SP().SeqSet(seqSet).SP()
+	enc.SP().NumSet(seqSet).SP()
 	if options != nil && options.UnchangedSince != 0 {
 		enc.Special('(').Atom("UNCHANGEDSINCE").SP().ModSeq(options.UnchangedSince).Special(')').SP()
 	}
@@ -39,13 +39,13 @@ func (c *Client) store(uid bool, seqSet imap.SeqSet, store *imap.StoreFlags, opt
 // Unless StoreFlags.Silent is set, the server will return the updated values.
 //
 // A nil options pointer is equivalent to a zero options value.
-func (c *Client) Store(seqSet imap.SeqSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
+func (c *Client) Store(seqSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
 	return c.store(false, seqSet, store, options)
 }
 
 // UIDStore sends a UID STORE command.
 //
 // See Store.
-func (c *Client) UIDStore(seqSet imap.SeqSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
+func (c *Client) UIDStore(seqSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
 	return c.store(true, seqSet, store, options)
 }
diff --git a/imapserver/append.go b/imapserver/append.go
index 459f404..38f59da 100644
--- a/imapserver/append.go
+++ b/imapserver/append.go
@@ -94,7 +94,7 @@ func (c *Conn) writeAppendOK(tag string, data *imap.AppendData) error {
 	enc.Atom(tag).SP().Atom("OK").SP()
 	if data != nil {
 		enc.Special('[')
-		enc.Atom("APPENDUID").SP().Number(data.UIDValidity).SP().Number(data.UID)
+		enc.Atom("APPENDUID").SP().Number(data.UIDValidity).SP().UID(data.UID)
 		enc.Special(']').SP()
 	}
 	enc.Text("APPEND completed")
diff --git a/imapserver/conn.go b/imapserver/conn.go
index e3d8179..56ba9e1 100644
--- a/imapserver/conn.go
+++ b/imapserver/conn.go
@@ -588,7 +588,7 @@ func (w *UpdateWriter) WriteMailboxFlags(flags []imap.Flag) error {
 }
 
 // WriteMessageFlags writes a FETCH response with FLAGS.
-func (w *UpdateWriter) WriteMessageFlags(seqNum, uid uint32, flags []imap.Flag) error {
+func (w *UpdateWriter) WriteMessageFlags(seqNum uint32, uid imap.UID, flags []imap.Flag) error {
 	fetchWriter := &FetchWriter{conn: w.conn}
 	respWriter := fetchWriter.CreateMessage(seqNum)
 	if uid != 0 {
diff --git a/imapserver/copy.go b/imapserver/copy.go
index cd321b2..c01864b 100644
--- a/imapserver/copy.go
+++ b/imapserver/copy.go
@@ -40,15 +40,15 @@ func (c *Conn) writeCopyOK(tag string, data *imap.CopyData) error {
 	enc.Atom(tag).SP().Atom("OK").SP()
 	if data != nil {
 		enc.Special('[')
-		enc.Atom("COPYUID").SP().Number(data.UIDValidity).SP().SeqSet(data.SourceUIDs).SP().SeqSet(data.DestUIDs)
+		enc.Atom("COPYUID").SP().Number(data.UIDValidity).SP().NumSet(data.SourceUIDs).SP().NumSet(data.DestUIDs)
 		enc.Special(']').SP()
 	}
 	enc.Text("COPY completed")
 	return enc.CRLF()
 }
 
-func readCopy(dec *imapwire.Decoder) (seqSet imap.SeqSet, dest string, err error) {
-	if !dec.ExpectSP() || !dec.ExpectSeqSet(&seqSet) || !dec.ExpectSP() || !dec.ExpectMailbox(&dest) || !dec.ExpectCRLF() {
+func readCopy(dec *imapwire.Decoder) (seqSet imap.NumSet, dest string, err error) {
+	if !dec.ExpectSP() || !dec.ExpectNumSet(&seqSet) || !dec.ExpectSP() || !dec.ExpectMailbox(&dest) || !dec.ExpectCRLF() {
 		return nil, "", dec.Err()
 	}
 	return seqSet, dest, nil
diff --git a/imapserver/expunge.go b/imapserver/expunge.go
index b281493..78549d7 100644
--- a/imapserver/expunge.go
+++ b/imapserver/expunge.go
@@ -13,14 +13,14 @@ func (c *Conn) handleExpunge(dec *imapwire.Decoder) error {
 }
 
 func (c *Conn) handleUIDExpunge(dec *imapwire.Decoder) error {
-	var seqSet imap.SeqSet
-	if !dec.ExpectSP() || !dec.ExpectSeqSet(&seqSet) || !dec.ExpectCRLF() {
+	var seqSet imap.NumSet
+	if !dec.ExpectSP() || !dec.ExpectNumSet(&seqSet) || !dec.ExpectCRLF() {
 		return dec.Err()
 	}
 	return c.expunge(&seqSet)
 }
 
-func (c *Conn) expunge(uids *imap.SeqSet) error {
+func (c *Conn) expunge(uids *imap.NumSet) error {
 	if err := c.checkState(imap.ConnStateSelected); err != nil {
 		return err
 	}
diff --git a/imapserver/fetch.go b/imapserver/fetch.go
index cefd428..73b5933 100644
--- a/imapserver/fetch.go
+++ b/imapserver/fetch.go
@@ -24,8 +24,8 @@ type fetchWriterOptions struct {
 }
 
 func (c *Conn) handleFetch(dec *imapwire.Decoder, numKind NumKind) error {
-	var seqSet imap.SeqSet
-	if !dec.ExpectSP() || !dec.ExpectSeqSet(&seqSet) || !dec.ExpectSP() {
+	var seqSet imap.NumSet
+	if !dec.ExpectSP() || !dec.ExpectNumSet(&seqSet) || !dec.ExpectSP() {
 		return dec.Err()
 	}
 
@@ -349,9 +349,9 @@ func (w *FetchResponseWriter) writeItemSep() {
 }
 
 // WriteUID writes the message's UID.
-func (w *FetchResponseWriter) WriteUID(uid uint32) {
+func (w *FetchResponseWriter) WriteUID(uid imap.UID) {
 	w.writeItemSep()
-	w.enc.Atom("UID").SP().Number(uid)
+	w.enc.Atom("UID").SP().UID(uid)
 }
 
 // WriteFlags writes the message's flags.
diff --git a/imapserver/imapmemserver/mailbox.go b/imapserver/imapmemserver/mailbox.go
index 6c3f909..33a861d 100644
--- a/imapserver/imapmemserver/mailbox.go
+++ b/imapserver/imapmemserver/mailbox.go
@@ -22,7 +22,7 @@ type Mailbox struct {
 	name       string
 	subscribed bool
 	l          []*message
-	uidNext    uint32
+	uidNext    imap.UID
 }
 
 // NewMailbox creates a new mailbox.
@@ -81,7 +81,7 @@ func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusD
 	}
 	if options.NumDeleted {
 		num := mbox.countByFlagLocked(imap.FlagDeleted)
-		data.NumUnseen = &num
+		data.NumDeleted = &num
 	}
 	if options.Size {
 		size := mbox.sizeLocked()
@@ -203,11 +203,11 @@ func (mbox *Mailbox) flagsLocked() []imap.Flag {
 	return l
 }
 
-func (mbox *Mailbox) Expunge(w *imapserver.ExpungeWriter, uids *imap.SeqSet) error {
+func (mbox *Mailbox) Expunge(w *imapserver.ExpungeWriter, uids *imap.NumSet) error {
 	expunged := make(map[*message]struct{})
 	mbox.mutex.Lock()
 	for _, msg := range mbox.l {
-		if uids != nil && !uids.Contains(msg.uid) {
+		if uids != nil && !uids.Contains(uint32(msg.uid)) {
 			continue
 		}
 		if _, ok := msg.flags[canonicalFlag(imap.FlagDeleted)]; ok {
@@ -282,7 +282,7 @@ func (mbox *MailboxView) Close() {
 	mbox.tracker.Close()
 }
 
-func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numKind imapserver.NumKind, seqSet imap.SeqSet, options *imap.FetchOptions) error {
+func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numKind imapserver.NumKind, seqSet imap.NumSet, options *imap.FetchOptions) error {
 	markSeen := false
 	for _, bs := range options.BodySection {
 		if !bs.Peek {
@@ -313,10 +313,10 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc
 	defer mbox.mutex.Unlock()
 
 	for _, seqSet := range criteria.SeqNum {
-		mbox.staticSeqSet(seqSet, imapserver.NumKindSeq)
+		mbox.staticNumSet(seqSet, imapserver.NumKindSeq)
 	}
 	for _, seqSet := range criteria.UID {
-		mbox.staticSeqSet(seqSet, imapserver.NumKindUID)
+		mbox.staticNumSet(seqSet, imapserver.NumKindUID)
 	}
 
 	data := imap.SearchData{
@@ -335,7 +335,7 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc
 		case imapserver.NumKindSeq:
 			num = seqNum
 		case imapserver.NumKindUID:
-			num = msg.uid
+			num = uint32(msg.uid)
 		}
 		if num == 0 {
 			continue
@@ -353,7 +353,7 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc
 	return &data, nil
 }
 
-func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numKind imapserver.NumKind, seqSet imap.SeqSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
+func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numKind imapserver.NumKind, seqSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
 	mbox.forEach(numKind, seqSet, func(seqNum uint32, msg *message) {
 		msg.store(flags)
 		mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker)
@@ -372,16 +372,16 @@ func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{})
 	return mbox.tracker.Idle(w, stop)
 }
 
-func (mbox *MailboxView) forEach(numKind imapserver.NumKind, seqSet imap.SeqSet, f func(seqNum uint32, msg *message)) {
+func (mbox *MailboxView) forEach(numKind imapserver.NumKind, seqSet imap.NumSet, f func(seqNum uint32, msg *message)) {
 	mbox.mutex.Lock()
 	defer mbox.mutex.Unlock()
 	mbox.forEachLocked(numKind, seqSet, f)
 }
 
-func (mbox *MailboxView) forEachLocked(numKind imapserver.NumKind, seqSet imap.SeqSet, f func(seqNum uint32, msg *message)) {
+func (mbox *MailboxView) forEachLocked(numKind imapserver.NumKind, seqSet imap.NumSet, f func(seqNum uint32, msg *message)) {
 	// TODO: optimize
 
-	mbox.staticSeqSet(seqSet, numKind)
+	mbox.staticNumSet(seqSet, numKind)
 
 	for i, msg := range mbox.l {
 		seqNum := uint32(i) + 1
@@ -391,7 +391,7 @@ func (mbox *MailboxView) forEachLocked(numKind imapserver.NumKind, seqSet imap.S
 		case imapserver.NumKindSeq:
 			num = mbox.tracker.EncodeSeqNum(seqNum)
 		case imapserver.NumKindUID:
-			num = msg.uid
+			num = uint32(msg.uid)
 		}
 		if num == 0 || !seqSet.Contains(num) {
 			continue
@@ -401,17 +401,17 @@ func (mbox *MailboxView) forEachLocked(numKind imapserver.NumKind, seqSet imap.S
 	}
 }
 
-// staticSeqSet converts a dynamic sequence set into a static one.
+// staticNumSet converts a dynamic sequence set into a static one.
 //
 // This is necessary to properly handle the special symbol "*", which
 // represents the maximum sequence number or UID in the mailbox.
-func (mbox *MailboxView) staticSeqSet(seqSet imap.SeqSet, numKind imapserver.NumKind) {
+func (mbox *MailboxView) staticNumSet(seqSet imap.NumSet, numKind imapserver.NumKind) {
 	var max uint32
 	switch numKind {
 	case imapserver.NumKindSeq:
 		max = uint32(len(mbox.l))
 	case imapserver.NumKindUID:
-		max = mbox.uidNext - 1
+		max = uint32(mbox.uidNext) - 1
 	}
 
 	for i := range seqSet {
diff --git a/imapserver/imapmemserver/message.go b/imapserver/imapmemserver/message.go
index 0041028..5b87f50 100644
--- a/imapserver/imapmemserver/message.go
+++ b/imapserver/imapmemserver/message.go
@@ -19,7 +19,7 @@ import (
 
 type message struct {
 	// immutable
-	uid uint32
+	uid imap.UID
 	buf []byte
 	t   time.Time
 
@@ -245,7 +245,7 @@ func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool {
 		}
 	}
 	for _, seqSet := range criteria.UID {
-		if !seqSet.Contains(msg.uid) {
+		if !seqSet.Contains(uint32(msg.uid)) {
 			return false
 		}
 	}
diff --git a/imapserver/imapmemserver/session.go b/imapserver/imapmemserver/session.go
index b7204ba..b92fddc 100644
--- a/imapserver/imapmemserver/session.go
+++ b/imapserver/imapmemserver/session.go
@@ -50,7 +50,7 @@ func (sess *UserSession) Unselect() error {
 	return nil
 }
 
-func (sess *UserSession) Copy(numKind imapserver.NumKind, seqSet imap.SeqSet, destName string) (*imap.CopyData, error) {
+func (sess *UserSession) Copy(numKind imapserver.NumKind, seqSet imap.NumSet, destName string) (*imap.CopyData, error) {
 	dest, err := sess.user.mailbox(destName)
 	if err != nil {
 		return nil, &imap.Error{
@@ -65,11 +65,11 @@ func (sess *UserSession) Copy(numKind imapserver.NumKind, seqSet imap.SeqSet, de
 		}
 	}
 
-	var sourceUIDs, destUIDs imap.SeqSet
+	var sourceUIDs, destUIDs imap.NumSet
 	sess.mailbox.forEach(numKind, seqSet, func(seqNum uint32, msg *message) {
 		appendData := dest.copyMsg(msg)
-		sourceUIDs.AddNum(msg.uid)
-		destUIDs.AddNum(appendData.UID)
+		sourceUIDs.AddNum(uint32(msg.uid))
+		destUIDs.AddNum(uint32(appendData.UID))
 	})
 
 	return &imap.CopyData{
@@ -79,7 +79,7 @@ func (sess *UserSession) Copy(numKind imapserver.NumKind, seqSet imap.SeqSet, de
 	}, nil
 }
 
-func (sess *UserSession) Move(w *imapserver.MoveWriter, numKind imapserver.NumKind, seqSet imap.SeqSet, destName string) error {
+func (sess *UserSession) Move(w *imapserver.MoveWriter, numKind imapserver.NumKind, seqSet imap.NumSet, destName string) error {
 	dest, err := sess.user.mailbox(destName)
 	if err != nil {
 		return &imap.Error{
@@ -97,12 +97,12 @@ func (sess *UserSession) Move(w *imapserver.MoveWriter, numKind imapserver.NumKi
 	sess.mailbox.mutex.Lock()
 	defer sess.mailbox.mutex.Unlock()
 
-	var sourceUIDs, destUIDs imap.SeqSet
+	var sourceUIDs, destUIDs imap.NumSet
 	expunged := make(map[*message]struct{})
 	sess.mailbox.forEachLocked(numKind, seqSet, func(seqNum uint32, msg *message) {
 		appendData := dest.copyMsg(msg)
-		sourceUIDs.AddNum(msg.uid)
-		destUIDs.AddNum(appendData.UID)
+		sourceUIDs.AddNum(uint32(msg.uid))
+		destUIDs.AddNum(uint32(appendData.UID))
 		expunged[msg] = struct{}{}
 	})
 	seqNums := sess.mailbox.expungeLocked(expunged)
diff --git a/imapserver/search.go b/imapserver/search.go
index 2291c18..95ad0d5 100644
--- a/imapserver/search.go
+++ b/imapserver/search.go
@@ -104,7 +104,7 @@ func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.Sea
 		enc.SP().Atom("UID")
 	}
 	if options.ReturnAll && len(data.All) > 0 {
-		enc.SP().Atom("ALL").SP().SeqSet(data.All)
+		enc.SP().Atom("ALL").SP().NumSet(data.All)
 	}
 	if options.ReturnMin && data.Min > 0 {
 		enc.SP().Atom("MIN").SP().Number(data.Min)
@@ -118,7 +118,7 @@ func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.Sea
 	return enc.CRLF()
 }
 
-func (c *Conn) writeSearch(seqSet imap.SeqSet) error {
+func (c *Conn) writeSearch(seqSet imap.NumSet) error {
 	enc := newResponseEncoder(c)
 	defer enc.end()
 
@@ -181,8 +181,8 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
 	case "ALL":
 		// nothing to do
 	case "UID":
-		var seqSet imap.SeqSet
-		if !dec.ExpectSP() || !dec.ExpectSeqSet(&seqSet) {
+		var seqSet imap.NumSet
+		if !dec.ExpectSP() || !dec.ExpectNumSet(&seqSet) {
 			return dec.Err()
 		}
 		criteria.UID = append(criteria.UID, seqSet)
@@ -302,7 +302,7 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
 		}
 		criteria.Or = append(criteria.Or, or)
 	default:
-		seqSet, err := imap.ParseSeqSet(key)
+		seqSet, err := imapwire.ParseNumSet(key)
 		if err != nil {
 			return err
 		}
diff --git a/imapserver/select.go b/imapserver/select.go
index 7501a12..e9b6db4 100644
--- a/imapserver/select.go
+++ b/imapserver/select.go
@@ -130,11 +130,11 @@ func (c *Conn) writeUIDValidity(uidValidity uint32) error {
 	return enc.CRLF()
 }
 
-func (c *Conn) writeUIDNext(uidNext uint32) error {
+func (c *Conn) writeUIDNext(uidNext imap.UID) error {
 	enc := newResponseEncoder(c)
 	defer enc.end()
 	enc.Atom("*").SP().Atom("OK").SP()
-	enc.Special('[').Atom("UIDNEXT").SP().Number(uidNext).Special(']')
+	enc.Special('[').Atom("UIDNEXT").SP().UID(uidNext).Special(']')
 	enc.SP().Text("Predicted next UID")
 	return enc.CRLF()
 }
diff --git a/imapserver/session.go b/imapserver/session.go
index 41742c1..5476430 100644
--- a/imapserver/session.go
+++ b/imapserver/session.go
@@ -64,11 +64,11 @@ type Session interface {
 
 	// Selected state
 	Unselect() error
-	Expunge(w *ExpungeWriter, uids *imap.SeqSet) error
+	Expunge(w *ExpungeWriter, uids *imap.NumSet) error
 	Search(kind NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error)
-	Fetch(w *FetchWriter, kind NumKind, seqSet imap.SeqSet, options *imap.FetchOptions) error
-	Store(w *FetchWriter, kind NumKind, seqSet imap.SeqSet, flags *imap.StoreFlags, options *imap.StoreOptions) error
-	Copy(kind NumKind, seqSet imap.SeqSet, dest string) (*imap.CopyData, error)
+	Fetch(w *FetchWriter, kind NumKind, seqSet imap.NumSet, options *imap.FetchOptions) error
+	Store(w *FetchWriter, kind NumKind, seqSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error
+	Copy(kind NumKind, seqSet imap.NumSet, dest string) (*imap.CopyData, error)
 }
 
 // SessionNamespace is an IMAP session which supports NAMESPACE.
@@ -84,7 +84,7 @@ type SessionMove interface {
 	Session
 
 	// Selected state
-	Move(w *MoveWriter, kind NumKind, seqSet imap.SeqSet, dest string) error
+	Move(w *MoveWriter, kind NumKind, seqSet imap.NumSet, dest string) error
 }
 
 // SessionIMAP4rev2 is an IMAP session which supports IMAP4rev2.
diff --git a/imapserver/status.go b/imapserver/status.go
index 62d1a46..806c260 100644
--- a/imapserver/status.go
+++ b/imapserver/status.go
@@ -54,7 +54,7 @@ func (c *Conn) writeStatus(data *imap.StatusData, options *imap.StatusOptions, r
 		listEnc.Item().Atom("MESSAGES").SP().Number(*data.NumMessages)
 	}
 	if options.UIDNext {
-		listEnc.Item().Atom("UIDNEXT").SP().Number(data.UIDNext)
+		listEnc.Item().Atom("UIDNEXT").SP().UID(data.UIDNext)
 	}
 	if options.UIDValidity {
 		listEnc.Item().Atom("UIDVALIDITY").SP().Number(data.UIDValidity)
diff --git a/imapserver/store.go b/imapserver/store.go
index 5650f61..2b7704c 100644
--- a/imapserver/store.go
+++ b/imapserver/store.go
@@ -10,10 +10,10 @@ import (
 
 func (c *Conn) handleStore(dec *imapwire.Decoder, numKind NumKind) error {
 	var (
-		seqSet imap.SeqSet
+		seqSet imap.NumSet
 		item   string
 	)
-	if !dec.ExpectSP() || !dec.ExpectSeqSet(&seqSet) || !dec.ExpectSP() || !dec.ExpectAtom(&item) || !dec.ExpectSP() {
+	if !dec.ExpectSP() || !dec.ExpectNumSet(&seqSet) || !dec.ExpectSP() || !dec.ExpectAtom(&item) || !dec.ExpectSP() {
 		return dec.Err()
 	}
 	var flags []imap.Flag
diff --git a/imapserver/tracker.go b/imapserver/tracker.go
index 55cd9f2..b22958c 100644
--- a/imapserver/tracker.go
+++ b/imapserver/tracker.go
@@ -89,7 +89,7 @@ func (t *MailboxTracker) QueueMailboxFlags(flags []imap.Flag) {
 // QueueMessageFlags queues a new FETCH FLAGS update.
 //
 // If source is not nil, the update won't be dispatched to it.
-func (t *MailboxTracker) QueueMessageFlags(seqNum, uid uint32, flags []imap.Flag, source *SessionTracker) {
+func (t *MailboxTracker) QueueMessageFlags(seqNum uint32, uid imap.UID, flags []imap.Flag, source *SessionTracker) {
 	t.queueUpdate(&trackerUpdate{fetch: &trackerUpdateFetch{
 		seqNum: seqNum,
 		uid:    uid,
@@ -106,7 +106,7 @@ type trackerUpdate struct {
 
 type trackerUpdateFetch struct {
 	seqNum uint32
-	uid    uint32
+	uid    imap.UID
 	flags  []imap.Flag
 }
 
diff --git a/internal/imapwire/decoder.go b/internal/imapwire/decoder.go
index b201d2c..3dfe8e2 100644
--- a/internal/imapwire/decoder.go
+++ b/internal/imapwire/decoder.go
@@ -478,24 +478,33 @@ func (dec *Decoder) ExpectMailbox(ptr *string) bool {
 	return dec.returnErr(err)
 }
 
-func (dec *Decoder) ExpectSeqSet(ptr *imap.SeqSet) bool {
+func (dec *Decoder) ExpectUID(ptr *imap.UID) bool {
+	var num uint32
+	if !dec.ExpectNumber(&num) {
+		return false
+	}
+	*ptr = imap.UID(num)
+	return true
+}
+
+func (dec *Decoder) ExpectNumSet(ptr *imap.NumSet) bool {
 	if dec.Special('$') {
 		*ptr = imap.SearchRes()
 		return true
 	}
 
 	var s string
-	if !dec.Expect(dec.Func(&s, isSeqSetChar), "sequence-set") {
+	if !dec.Expect(dec.Func(&s, isNumSetChar), "sequence-set") {
 		return false
 	}
-	seqSet, err := imap.ParseSeqSet(s)
+	seqSet, err := ParseNumSet(s)
 	if err == nil {
 		*ptr = seqSet
 	}
 	return dec.returnErr(err)
 }
 
-func isSeqSetChar(ch byte) bool {
+func isNumSetChar(ch byte) bool {
 	return ch == '*' || IsAtomChar(ch)
 }
 
diff --git a/internal/imapwire/encoder.go b/internal/imapwire/encoder.go
index 51277ff..1166f41 100644
--- a/internal/imapwire/encoder.go
+++ b/internal/imapwire/encoder.go
@@ -158,7 +158,7 @@ func (enc *Encoder) Mailbox(name string) *Encoder {
 	}
 }
 
-func (enc *Encoder) SeqSet(seqSet imap.SeqSet) *Encoder {
+func (enc *Encoder) NumSet(seqSet imap.NumSet) *Encoder {
 	s := seqSet.String()
 	if s == "" {
 		enc.setErr(fmt.Errorf("imapwire: cannot encode empty sequence set"))
@@ -241,6 +241,10 @@ func (enc *Encoder) Text(s string) *Encoder {
 	return enc.writeString(s)
 }
 
+func (enc *Encoder) UID(uid imap.UID) *Encoder {
+	return enc.Number(uint32(uid))
+}
+
 // Literal writes a literal.
 //
 // The caller must write exactly size bytes to the returned writer.
diff --git a/internal/imapwire/numset.go b/internal/imapwire/numset.go
new file mode 100644
index 0000000..a3dd2c9
--- /dev/null
+++ b/internal/imapwire/numset.go
@@ -0,0 +1,63 @@
+package imapwire
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/emersion/go-imap/v2"
+)
+
+// errBadNumSet is used to report problems with the format of a number set
+// value.
+type errBadNumSet string
+
+func (err errBadNumSet) Error() string {
+	return fmt.Sprintf("imap: bad number set value %q", string(err))
+}
+
+// parseNum parses a single seq-number value (non-zero uint32 or "*").
+func parseNum(v string) (uint32, error) {
+	if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' {
+		return uint32(n), nil
+	} else if v == "*" {
+		return 0, nil
+	}
+	return 0, errBadNumSet(v)
+}
+
+// parseNumRange creates a new seq instance by parsing strings in the format
+// "n" or "n:m", where n and/or m may be "*". An error is returned for invalid
+// values.
+func parseNumRange(v string) (imap.NumRange, error) {
+	var (
+		s   imap.NumRange
+		err error
+	)
+	if sep := strings.IndexRune(v, ':'); sep < 0 {
+		s.Start, err = parseNum(v)
+		s.Stop = s.Start
+		return s, err
+	} else if s.Start, err = parseNum(v[:sep]); err == nil {
+		if s.Stop, err = parseNum(v[sep+1:]); err == nil {
+			if (s.Stop < s.Start && s.Stop != 0) || s.Start == 0 {
+				s.Start, s.Stop = s.Stop, s.Start
+			}
+			return s, nil
+		}
+	}
+	return s, errBadNumSet(v)
+}
+
+// ParseNumSet returns a new NumSet after parsing the set string.
+func ParseNumSet(set string) (imap.NumSet, error) {
+	var s imap.NumSet
+	for _, sv := range strings.Split(set, ",") {
+		v, err := parseNumRange(sv)
+		if err != nil {
+			return s, err
+		}
+		s.AddRange(v.Start, v.Stop)
+	}
+	return s, nil
+}
diff --git a/seqset.go b/numset.go
similarity index 60%
rename from seqset.go
rename to numset.go
index 84fd5ab..faa4e27 100644
--- a/seqset.go
+++ b/numset.go
@@ -1,64 +1,22 @@
 package imap
 
 import (
-	"fmt"
 	"strconv"
-	"strings"
 )
 
-// errBadSeqSet is used to report problems with the format of a sequence set
-// value.
-type errBadSeqSet string
-
-func (err errBadSeqSet) Error() string {
-	return fmt.Sprintf("imap: bad sequence set value %q", string(err))
-}
-
-// Seq represents a single seq-number or seq-range value (RFC 3501 ABNF). Values
+// NumRange represents a single seq-number or seq-range value (RFC 3501 ABNF). Values
 // may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is
 // represented by setting Start = Stop. Zero is used to represent "*", which is
 // safe because seq-number uses nz-number rule. The order of values is always
 // Start <= Stop, except when representing "n:*", where Start = n and Stop = 0.
-type Seq struct {
+type NumRange struct {
 	Start, Stop uint32
 }
 
-// parseSeqNum parses a single seq-number value (non-zero uint32 or "*").
-func parseSeqNum(v string) (uint32, error) {
-	if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' {
-		return uint32(n), nil
-	} else if v == "*" {
-		return 0, nil
-	}
-	return 0, errBadSeqSet(v)
-}
-
-// parseSeq creates a new seq instance by parsing strings in the format "n" or
-// "n:m", where n and/or m may be "*". An error is returned for invalid values.
-func parseSeq(v string) (Seq, error) {
-	var (
-		s   Seq
-		err error
-	)
-	if sep := strings.IndexRune(v, ':'); sep < 0 {
-		s.Start, err = parseSeqNum(v)
-		s.Stop = s.Start
-		return s, err
-	} else if s.Start, err = parseSeqNum(v[:sep]); err == nil {
-		if s.Stop, err = parseSeqNum(v[sep+1:]); err == nil {
-			if (s.Stop < s.Start && s.Stop != 0) || s.Start == 0 {
-				s.Start, s.Stop = s.Stop, s.Start
-			}
-			return s, nil
-		}
-	}
-	return s, errBadSeqSet(v)
-}
-
-// Contains returns true if the seq-number q is contained in sequence value s.
+// Contains returns true if the seq-number q is contained in range value s.
 // The dynamic value "*" contains only other "*" values, the dynamic range "n:*"
 // contains "*" and all numbers >= n.
-func (s Seq) Contains(q uint32) bool {
+func (s NumRange) Contains(q uint32) bool {
 	if q == 0 {
 		return s.Stop == 0 // "*" is contained only in "*" and "n:*"
 	}
@@ -66,15 +24,15 @@ func (s Seq) Contains(q uint32) bool {
 }
 
 // Less returns true if s precedes and does not contain seq-number q.
-func (s Seq) Less(q uint32) bool {
+func (s NumRange) Less(q uint32) bool {
 	return (s.Stop < q || q == 0) && s.Stop != 0
 }
 
-// Merge combines sequence values s and t into a single union if the two
+// Merge combines range values s and t into a single union if the two
 // intersect or one is a superset of the other. The order of s and t does not
 // matter. If the values cannot be merged, s is returned unmodified and ok is
 // set to false.
-func (s Seq) Merge(t Seq) (union Seq, ok bool) {
+func (s NumRange) Merge(t NumRange) (union NumRange, ok bool) {
 	union = s
 	if s == t {
 		return s, true
@@ -90,7 +48,7 @@ func (s Seq) Merge(t Seq) (union Seq, ok bool) {
 		}
 		// s is "n" or "n:m", if m == ^uint32(0) then t is "n:*"
 		if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) {
-			return Seq{s.Start, t.Stop}, true // s intersects or touches t
+			return NumRange{s.Start, t.Stop}, true // s intersects or touches t
 		}
 		return union, false
 	}
@@ -105,8 +63,8 @@ func (s Seq) Merge(t Seq) (union Seq, ok bool) {
 	return union, false
 }
 
-// String returns sequence value s as a seq-number or seq-range string.
-func (s Seq) String() string {
+// String returns range value s as a seq-number or seq-range string.
+func (s NumRange) String() string {
 	if s.Start == s.Stop {
 		if s.Start == 0 {
 			return "*"
@@ -120,7 +78,7 @@ func (s Seq) String() string {
 	return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10))
 }
 
-func (s Seq) append(nums []uint32) (out []uint32, ok bool) {
+func (s NumRange) append(nums []uint32) (out []uint32, ok bool) {
 	if s.Start == 0 || s.Stop == 0 {
 		return nil, false
 	}
@@ -130,62 +88,49 @@ func (s Seq) append(nums []uint32) (out []uint32, ok bool) {
 	return nums, true
 }
 
-// SeqSet is used to represent a set of message sequence numbers or UIDs (see
+// NumSet is used to represent a set of message sequence numbers or UIDs (see
 // sequence-set ABNF rule). The zero value is an empty set.
-type SeqSet []Seq
-
-// ParseSeqSet returns a new SeqSet after parsing the set string.
-func ParseSeqSet(set string) (SeqSet, error) {
-	var s SeqSet
-	for _, sv := range strings.Split(set, ",") {
-		v, err := parseSeq(sv)
-		if err != nil {
-			return s, err
-		}
-		s.insert(v)
-	}
-	return s, nil
-}
+type NumSet []NumRange
 
-// SeqSetNum returns a new SeqSet containing the sequence numbers.
-func SeqSetNum(q ...uint32) SeqSet {
-	var s SeqSet
+// NumSetNum returns a new NumSet containing the specified numbers.
+func NumSetNum(q ...uint32) NumSet {
+	var s NumSet
 	s.AddNum(q...)
 	return s
 }
 
-// SeqSetRange returns a new SeqSet containing the sequence range.
-func SeqSetRange(start, stop uint32) SeqSet {
-	var s SeqSet
+// NumSetRange returns a new NumSet containing the specified range.
+func NumSetRange(start, stop uint32) NumSet {
+	var s NumSet
 	s.AddRange(start, stop)
 	return s
 }
 
-// AddNum inserts new sequence numbers into the set. The value 0 represents "*".
-func (s *SeqSet) AddNum(q ...uint32) {
+// AddNum inserts new numbers into the set. The value 0 represents "*".
+func (s *NumSet) AddNum(q ...uint32) {
 	for _, v := range q {
-		s.insert(Seq{v, v})
+		s.insert(NumRange{v, v})
 	}
 }
 
-// AddRange inserts a new sequence range into the set.
-func (s *SeqSet) AddRange(start, stop uint32) {
+// AddRange inserts a new range into the set.
+func (s *NumSet) AddRange(start, stop uint32) {
 	if (stop < start && stop != 0) || start == 0 {
-		s.insert(Seq{stop, start})
+		s.insert(NumRange{stop, start})
 	} else {
-		s.insert(Seq{start, stop})
+		s.insert(NumRange{start, stop})
 	}
 }
 
 // AddSet inserts all values from t into s.
-func (s *SeqSet) AddSet(t SeqSet) {
+func (s *NumSet) AddSet(t NumSet) {
 	for _, v := range t {
 		s.insert(v)
 	}
 }
 
 // Dynamic returns true if the set contains "*" or "n:*" values.
-func (s SeqSet) Dynamic() bool {
+func (s NumSet) Dynamic() bool {
 	return len(s) > 0 && s[len(s)-1].Stop == 0
 }
 
@@ -194,15 +139,15 @@ func (s SeqSet) Dynamic() bool {
 // responsibility to handle the special case where q is the maximum UID in the
 // mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since
 // it doesn't know what the maximum value is).
-func (s SeqSet) Contains(q uint32) bool {
+func (s NumSet) Contains(q uint32) bool {
 	if _, ok := s.search(q); ok {
 		return q != 0
 	}
 	return false
 }
 
-// Nums returns a slice of all numbers contained in the sequence set.
-func (s SeqSet) Nums() (nums []uint32, ok bool) {
+// Nums returns a slice of all numbers contained in the set.
+func (s NumSet) Nums() (nums []uint32, ok bool) {
 	for _, v := range s {
 		nums, ok = v.append(nums)
 		if !ok {
@@ -212,8 +157,8 @@ func (s SeqSet) Nums() (nums []uint32, ok bool) {
 	return nums, true
 }
 
-// String returns a sorted representation of all contained sequence values.
-func (s SeqSet) String() string {
+// String returns a sorted representation of all contained number values.
+func (s NumSet) String() string {
 	if IsSearchRes(s) {
 		return "$"
 	}
@@ -239,8 +184,8 @@ func (s SeqSet) String() string {
 	return string(b[1:])
 }
 
-// insert adds sequence value v to the set.
-func (ptr *SeqSet) insert(v Seq) {
+// insert adds range value v to the set.
+func (ptr *NumSet) insert(v NumRange) {
 	s := *ptr
 	defer func() {
 		*ptr = s
@@ -278,8 +223,8 @@ func (ptr *SeqSet) insert(v Seq) {
 	s = s[:i+1]
 }
 
-// insertAt inserts a new sequence value v at index i, resizing s.Set as needed.
-func (ptr *SeqSet) insertAt(i int, v Seq) {
+// insertAt inserts a new range value v at index i, resizing s.Set as needed.
+func (ptr *NumSet) insertAt(i int, v NumRange) {
 	s := *ptr
 	defer func() {
 		*ptr = s
@@ -295,7 +240,7 @@ func (ptr *SeqSet) insertAt(i int, v Seq) {
 		copy(s[i+1:], s[i:])
 	} else {
 		// allocate new slice and copy everything, n is at least 1
-		set := make([]Seq, n+1, n*2)
+		set := make([]NumRange, n+1, n*2)
 		copy(set, s[:i])
 		copy(set[i+1:], s[i:])
 		s = set
@@ -303,10 +248,10 @@ func (ptr *SeqSet) insertAt(i int, v Seq) {
 	s[i] = v
 }
 
-// search attempts to find the index of the sequence set value that contains q.
+// search attempts to find the index of the range set value that contains q.
 // If no values contain q, the returned index is the position where q should be
 // inserted and ok is set to false.
-func (s SeqSet) search(q uint32) (i int, ok bool) {
+func (s NumSet) search(q uint32) (i int, ok bool) {
 	min, max := 0, len(s)-1
 	for min < max {
 		if mid := (min + max) >> 1; s[mid].Less(q) {
diff --git a/search.go b/search.go
index 08da6a8..83e0d20 100644
--- a/search.go
+++ b/search.go
@@ -36,8 +36,8 @@ type SearchOptions struct {
 //		{Body: []string{"world"}},
 //	}}}
 type SearchCriteria struct {
-	SeqNum []SeqSet
-	UID    []SeqSet
+	SeqNum []NumSet
+	UID    []NumSet
 
 	// Only the date is used, the time and timezone are ignored
 	Since      time.Time
@@ -135,7 +135,7 @@ const (
 
 // SearchData is the data returned by a SEARCH command.
 type SearchData struct {
-	All SeqSet
+	All NumSet
 
 	// requires IMAP4rev2 or ESEARCH
 	UID   bool
@@ -154,23 +154,23 @@ func (data *SearchData) AllNums() []uint32 {
 	return nums
 }
 
-// searchRes is a special empty SeqSet which can be used as a marker. It has
+// searchRes is a special empty NumSet which can be used as a marker. It has
 // a non-zero cap so that its data pointer is non-nil and can be compared.
 var (
-	searchRes     = make(SeqSet, 0, 1)
+	searchRes     = make(NumSet, 0, 1)
 	searchResAddr = reflect.ValueOf(searchRes).Pointer()
 )
 
-// SearchRes returns a special marker which can be used instead of a SeqSet to
+// SearchRes returns a special marker which can be used instead of a NumSet to
 // reference the last SEARCH result. On the wire, it's encoded as '$'.
 //
 // It requires IMAP4rev2 or the SEARCHRES extension.
-func SearchRes() SeqSet {
+func SearchRes() NumSet {
 	return searchRes
 }
 
 // IsSearchRes checks whether a sequence set is a reference to the last SEARCH
 // result. See SearchRes.
-func IsSearchRes(seqSet SeqSet) bool {
+func IsSearchRes(seqSet NumSet) bool {
 	return reflect.ValueOf(seqSet).Pointer() == searchResAddr
 }
diff --git a/select.go b/select.go
index 427df4c..9e3359f 100644
--- a/select.go
+++ b/select.go
@@ -16,7 +16,7 @@ type SelectData struct {
 	PermanentFlags []Flag
 	// Number of messages in this mailbox (aka. "EXISTS")
 	NumMessages uint32
-	UIDNext     uint32
+	UIDNext     UID
 	UIDValidity uint32
 
 	List *ListData // requires IMAP4rev2
diff --git a/status.go b/status.go
index a74e084..223eb6d 100644
--- a/status.go
+++ b/status.go
@@ -21,7 +21,7 @@ type StatusData struct {
 	Mailbox string
 
 	NumMessages *uint32
-	UIDNext     uint32
+	UIDNext     UID
 	UIDValidity uint32
 	NumUnseen   *uint32
 	NumDeleted  *uint32


Test patch:
diff --git a/imapclient/client_test.go b/imapclient/client_test.go
index 50105d0..1c732f4 100644
--- a/imapclient/client_test.go
+++ b/imapclient/client_test.go
@@ -3,6 +3,7 @@ package imapclient_test
 import (
 	"io"
 	"net"
+	"strings"
 	"testing"
 
 	"github.com/emersion/go-imap/v2"
@@ -16,11 +17,24 @@ const (
 	testPassword = "test-password"
 )
 
+const simpleRawMessage = `MIME-Version: 1.0
+Message-Id: <191101702316132@example.com>
+Content-Transfer-Encoding: 8bit
+Content-Type: text/plain; charset=utf-8
+
+This is my letter!`
+
 func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient.Client, io.Closer) {
 	memServer := imapmemserver.New()
 
 	user := imapmemserver.NewUser(testUsername, testPassword)
 	user.Create("INBOX", nil)
+
+	_, err := user.Append("INBOX", strings.NewReader(simpleRawMessage), &imap.AppendOptions{})
+	if err != nil {
+		t.Fatalf("Append() = %v", err)
+	}
+
 	memServer.AddUser(user)
 
 	server := imapserver.New(&imapserver.Options{
@@ -72,6 +86,18 @@ func TestLogin(t *testing.T) {
 	}
 }
 
+func TestLogout(t *testing.T) {
+	client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
+	defer server.Close()
+
+	if err := client.Logout().Wait(); err != nil {
+		t.Errorf("Logout().Wait() = %v", err)
+	}
+	if err := client.Close(); err != nil {
+		t.Errorf("Close() = %v", err)
+	}
+}
+
 func TestIdle(t *testing.T) {
 	client, server := newClientServerPair(t, imap.ConnStateSelected)
 	defer client.Close()
@@ -93,8 +119,26 @@ func TestFetch_invalid(t *testing.T) {
 	defer client.Close()
 	defer server.Close()
 
-	_, err := client.UIDFetch(imap.SeqSetNum(), nil).Collect()
+	_, err := client.UIDFetch(imap.NumSetNum(), nil).Collect()
 	if err == nil {
 		t.Fatalf("UIDFetch().Collect() = %v", err)
 	}
 }
+
+func TestFetch_closeUnreadBody(t *testing.T) {
+	client, server := newClientServerPair(t, imap.ConnStateSelected)
+	defer client.Close()
+	defer server.Close()
+
+	fetchCmd := client.UIDFetch(imap.NumSetNum(1), &imap.FetchOptions{
+		BodySection: []*imap.FetchItemBodySection{
+			{
+				Specifier: imap.PartSpecifierNone,
+				Peek:      true,
+			},
+		},
+	})
+	if err := fetchCmd.Close(); err != nil {
+		t.Fatalf("UIDFetch().Close() = %v", err)
+	}
+}
diff --git a/imapclient/example_test.go b/imapclient/example_test.go
index b48e3bc..0cd0e74 100644
--- a/imapclient/example_test.go
+++ b/imapclient/example_test.go
@@ -38,7 +38,7 @@ func ExampleClient() {
 	log.Printf("INBOX contains %v messages", selectedMbox.NumMessages)
 
 	if selectedMbox.NumMessages > 0 {
-		seqSet := imap.SeqSetNum(1)
+		seqSet := imap.NumSetNum(1)
 		fetchOptions := &imap.FetchOptions{Envelope: true}
 		messages, err := c.Fetch(seqSet, fetchOptions).Collect()
 		if err != nil {
@@ -61,7 +61,7 @@ func ExampleClient_pipelining() {
 	// Login, select and fetch a message in a single roundtrip
 	loginCmd := c.Login("root", "root")
 	selectCmd := c.Select("INBOX", nil)
-	fetchCmd := c.UIDFetch(imap.SeqSetNum(uid), fetchOptions)
+	fetchCmd := c.UIDFetch(imap.NumSetNum(uid), fetchOptions)
 
 	if err := loginCmd.Wait(); err != nil {
 		log.Fatalf("failed to login: %v", err)
@@ -129,7 +129,7 @@ func ExampleClient_List_stream() {
 func ExampleClient_Store() {
 	var c *imapclient.Client
 
-	seqSet := imap.SeqSetNum(1)
+	seqSet := imap.NumSetNum(1)
 	storeFlags := imap.StoreFlags{
 		Op:     imap.StoreFlagsAdd,
 		Flags:  []imap.Flag{imap.FlagFlagged},
@@ -143,7 +143,7 @@ func ExampleClient_Store() {
 func ExampleClient_Fetch() {
 	var c *imapclient.Client
 
-	seqSet := imap.SeqSetNum(1)
+	seqSet := imap.NumSetNum(1)
 	fetchOptions := &imap.FetchOptions{
 		Flags:    true,
 		Envelope: true,
@@ -171,7 +171,7 @@ func ExampleClient_Fetch() {
 func ExampleClient_Fetch_stream() {
 	var c *imapclient.Client
 
-	seqSet := imap.SeqSetNum(1)
+	seqSet := imap.NumSetNum(1)
 	fetchOptions := &imap.FetchOptions{
 		UID:         true,
 		BodySection: []*imap.FetchItemBodySection{{}},
diff --git a/seqset_test.go b/internal/imapwire/numset_test.go
similarity index 79%
rename from seqset_test.go
rename to internal/imapwire/numset_test.go
index 78a85d3..91f244b 100644
--- a/seqset_test.go
+++ b/internal/imapwire/numset_test.go
@@ -1,86 +1,88 @@
-package imap
+package imapwire
 
 import (
 	"math/rand"
 	"strings"
 	"testing"
+
+	"github.com/emersion/go-imap/v2"
 )
 
 const max = ^uint32(0)
 
-func TestParseSeq(t *testing.T) {
+func TestParseNumRange(t *testing.T) {
 	tests := []struct {
 		in  string
-		out Seq
+		out imap.NumRange
 		ok  bool
 	}{
 		// Invalid number
-		{"", Seq{}, false},
-		{" ", Seq{}, false},
-		{"A", Seq{}, false},
-		{"0", Seq{}, false},
-		{" 1", Seq{}, false},
-		{"1 ", Seq{}, false},
-		{"*1", Seq{}, false},
-		{"1*", Seq{}, false},
-		{"-1", Seq{}, false},
-		{"01", Seq{}, false},
-		{"0x1", Seq{}, false},
-		{"1 2", Seq{}, false},
-		{"1,2", Seq{}, false},
-		{"1.2", Seq{}, false},
-		{"4294967296", Seq{}, false},
+		{"", imap.NumRange{}, false},
+		{" ", imap.NumRange{}, false},
+		{"A", imap.NumRange{}, false},
+		{"0", imap.NumRange{}, false},
+		{" 1", imap.NumRange{}, false},
+		{"1 ", imap.NumRange{}, false},
+		{"*1", imap.NumRange{}, false},
+		{"1*", imap.NumRange{}, false},
+		{"-1", imap.NumRange{}, false},
+		{"01", imap.NumRange{}, false},
+		{"0x1", imap.NumRange{}, false},
+		{"1 2", imap.NumRange{}, false},
+		{"1,2", imap.NumRange{}, false},
+		{"1.2", imap.NumRange{}, false},
+		{"4294967296", imap.NumRange{}, false},
 
 		// Valid number
-		{"*", Seq{0, 0}, true},
-		{"1", Seq{1, 1}, true},
-		{"42", Seq{42, 42}, true},
-		{"1000", Seq{1000, 1000}, true},
-		{"4294967295", Seq{max, max}, true},
+		{"*", imap.NumRange{0, 0}, true},
+		{"1", imap.NumRange{1, 1}, true},
+		{"42", imap.NumRange{42, 42}, true},
+		{"1000", imap.NumRange{1000, 1000}, true},
+		{"4294967295", imap.NumRange{max, max}, true},
 
 		// Invalid range
-		{":", Seq{}, false},
-		{"*:", Seq{}, false},
-		{":*", Seq{}, false},
-		{"1:", Seq{}, false},
-		{":1", Seq{}, false},
-		{"0:0", Seq{}, false},
-		{"0:*", Seq{}, false},
-		{"0:1", Seq{}, false},
-		{"1:0", Seq{}, false},
-		{"1:2 ", Seq{}, false},
-		{"1: 2", Seq{}, false},
-		{"1:2:", Seq{}, false},
-		{"1:2,", Seq{}, false},
-		{"1:2:3", Seq{}, false},
-		{"1:2,3", Seq{}, false},
-		{"*:4294967296", Seq{}, false},
-		{"0:4294967295", Seq{}, false},
-		{"1:4294967296", Seq{}, false},
-		{"4294967296:*", Seq{}, false},
-		{"4294967295:0", Seq{}, false},
-		{"4294967296:1", Seq{}, false},
-		{"4294967295:4294967296", Seq{}, false},
+		{":", imap.NumRange{}, false},
+		{"*:", imap.NumRange{}, false},
+		{":*", imap.NumRange{}, false},
+		{"1:", imap.NumRange{}, false},
+		{":1", imap.NumRange{}, false},
+		{"0:0", imap.NumRange{}, false},
+		{"0:*", imap.NumRange{}, false},
+		{"0:1", imap.NumRange{}, false},
+		{"1:0", imap.NumRange{}, false},
+		{"1:2 ", imap.NumRange{}, false},
+		{"1: 2", imap.NumRange{}, false},
+		{"1:2:", imap.NumRange{}, false},
+		{"1:2,", imap.NumRange{}, false},
+		{"1:2:3", imap.NumRange{}, false},
+		{"1:2,3", imap.NumRange{}, false},
+		{"*:4294967296", imap.NumRange{}, false},
+		{"0:4294967295", imap.NumRange{}, false},
+		{"1:4294967296", imap.NumRange{}, false},
+		{"4294967296:*", imap.NumRange{}, false},
+		{"4294967295:0", imap.NumRange{}, false},
+		{"4294967296:1", imap.NumRange{}, false},
+		{"4294967295:4294967296", imap.NumRange{}, false},
 
 		// Valid range
-		{"*:*", Seq{0, 0}, true},
-		{"1:*", Seq{1, 0}, true},
-		{"*:1", Seq{1, 0}, true},
-		{"2:2", Seq{2, 2}, true},
-		{"2:42", Seq{2, 42}, true},
-		{"42:2", Seq{2, 42}, true},
-		{"*:4294967294", Seq{max - 1, 0}, true},
-		{"*:4294967295", Seq{max, 0}, true},
-		{"4294967294:*", Seq{max - 1, 0}, true},
-		{"4294967295:*", Seq{max, 0}, true},
-		{"1:4294967294", Seq{1, max - 1}, true},
-		{"1:4294967295", Seq{1, max}, true},
-		{"4294967295:1000", Seq{1000, max}, true},
-		{"4294967294:4294967295", Seq{max - 1, max}, true},
-		{"4294967295:4294967295", Seq{max, max}, true},
+		{"*:*", imap.NumRange{0, 0}, true},
+		{"1:*", imap.NumRange{1, 0}, true},
+		{"*:1", imap.NumRange{1, 0}, true},
+		{"2:2", imap.NumRange{2, 2}, true},
+		{"2:42", imap.NumRange{2, 42}, true},
+		{"42:2", imap.NumRange{2, 42}, true},
+		{"*:4294967294", imap.NumRange{max - 1, 0}, true},
+		{"*:4294967295", imap.NumRange{max, 0}, true},
+		{"4294967294:*", imap.NumRange{max - 1, 0}, true},
+		{"4294967295:*", imap.NumRange{max, 0}, true},
+		{"1:4294967294", imap.NumRange{1, max - 1}, true},
+		{"1:4294967295", imap.NumRange{1, max}, true},
+		{"4294967295:1000", imap.NumRange{1000, max}, true},
+		{"4294967294:4294967295", imap.NumRange{max - 1, max}, true},
+		{"4294967295:4294967295", imap.NumRange{max, max}, true},
 	}
 	for _, test := range tests {
-		out, err := parseSeq(test.in)
+		out, err := parseNumRange(test.in)
 		if !test.ok {
 			if err == nil {
 				t.Errorf("parseSeq(%q) expected error; got %q", test.in, out)
@@ -93,7 +95,7 @@ func TestParseSeq(t *testing.T) {
 	}
 }
 
-func TestSeqContainsLess(t *testing.T) {
+func TestNumRangeContainsLess(t *testing.T) {
 	tests := []struct {
 		s        string
 		q        uint32
@@ -143,7 +145,7 @@ func TestSeqContainsLess(t *testing.T) {
 		{"4:*", max, true, false},
 	}
 	for _, test := range tests {
-		s, err := parseSeq(test.s)
+		s, err := parseNumRange(test.s)
 		if err != nil {
 			t.Errorf("parseSeq(%q) unexpected error; %v", test.s, err)
 			continue
@@ -157,7 +159,7 @@ func TestSeqContainsLess(t *testing.T) {
 	}
 }
 
-func TestSeqMerge(T *testing.T) {
+func TestNumRangeMerge(T *testing.T) {
 	tests := []struct {
 		s, t, out string
 	}{
@@ -186,7 +188,7 @@ func TestSeqMerge(T *testing.T) {
 		{"*", "4294967295", ""},
 		{"*", "*", "*"},
 
-		// Range with number
+		// NumRange with number
 		{"1:3", "1", "1:3"},
 		{"1:3", "2", "1:3"},
 		{"1:3", "3", "1:3"},
@@ -252,7 +254,7 @@ func TestSeqMerge(T *testing.T) {
 		{"1:*", "4294967295", "1:*"},
 		{"1:*", "*", "1:*"},
 
-		// Range with range
+		// NumRange with range
 		{"5:8", "1:2", ""},
 		{"5:8", "1:3", ""},
 		{"5:8", "1:4", "1:8"},
@@ -340,12 +342,12 @@ func TestSeqMerge(T *testing.T) {
 		{"1:4294967295", "2:*", "1:*"},
 	}
 	for _, test := range tests {
-		s, err := parseSeq(test.s)
+		s, err := parseNumRange(test.s)
 		if err != nil {
 			T.Errorf("parseSeq(%q) unexpected error; %v", test.s, err)
 			continue
 		}
-		t, err := parseSeq(test.t)
+		t, err := parseNumRange(test.t)
 		if err != nil {
 			T.Errorf("parseSeq(%q) unexpected error; %v", test.t, err)
 			continue
@@ -366,31 +368,31 @@ func TestSeqMerge(T *testing.T) {
 	}
 }
 
-func checkSeqSet(s SeqSet, t *testing.T) {
+func checkNumSet(s imap.NumSet, t *testing.T) {
 	n := len(s)
 	for i, v := range s {
 		if v.Start == 0 {
 			if v.Stop != 0 {
-				t.Errorf(`SeqSet(%q) index %d: "*:n" range`, s, i)
+				t.Errorf(`NumSet(%q) index %d: "*:n" range`, s, i)
 			} else if i != n-1 {
-				t.Errorf(`SeqSet(%q) index %d: "*" not at the end`, s, i)
+				t.Errorf(`NumSet(%q) index %d: "*" not at the end`, s, i)
 			}
 			continue
 		}
 		if i > 0 && s[i-1].Stop >= v.Start-1 {
-			t.Errorf(`SeqSet(%q) index %d: overlap`, s, i)
+			t.Errorf(`NumSet(%q) index %d: overlap`, s, i)
 		}
 		if v.Stop < v.Start {
 			if v.Stop != 0 {
-				t.Errorf(`SeqSet(%q) index %d: reversed range`, s, i)
+				t.Errorf(`NumSet(%q) index %d: reversed range`, s, i)
 			} else if i != n-1 {
-				t.Errorf(`SeqSet(%q) index %d: "n:*" not at the end`, s, i)
+				t.Errorf(`NumSet(%q) index %d: "n:*" not at the end`, s, i)
 			}
 		}
 	}
 }
 
-func TestSeqSetInfo(t *testing.T) {
+func TestNumSetInfo(t *testing.T) {
 	tests := []struct {
 		s        string
 		q        uint32
@@ -531,8 +533,8 @@ func TestSeqSetInfo(t *testing.T) {
 		{"1,3:5,7,9,42,60:70,100:*", max, true},
 	}
 	for _, test := range tests {
-		s, _ := ParseSeqSet(test.s)
-		checkSeqSet(s, t)
+		s, _ := ParseNumSet(test.s)
+		checkNumSet(s, t)
 		if s.Contains(test.q) != test.contains {
 			t.Errorf("%q.Contains(%v) expected %v", test.s, test.q, test.contains)
 		}
@@ -550,7 +552,7 @@ func TestSeqSetInfo(t *testing.T) {
 	}
 }
 
-func TestParseSeqSet(t *testing.T) {
+func TestParseNumSet(t *testing.T) {
 	tests := []struct {
 		in  string
 		out string
@@ -673,12 +675,12 @@ func TestParseSeqSet(t *testing.T) {
 	}
 	for _, test := range tests {
 		for i := 0; i < 100 && test.in != ""; i++ {
-			s, err := ParseSeqSet(test.in)
+			s, err := ParseNumSet(test.in)
 			if err != nil {
 				t.Errorf("Add(%q) unexpected error; %v", test.in, err)
 				i = 100
 			}
-			checkSeqSet(s, t)
+			checkNumSet(s, t)
 			if out := s.String(); out != test.out {
 				t.Errorf("%q.String() expected %q; got %q", test.in, test.out, out)
 				i = 100
@@ -688,34 +690,34 @@ func TestParseSeqSet(t *testing.T) {
 	}
 }
 
-func TestSeqSetAddNumRangeSet(t *testing.T) {
+func TestNumSetAddNumRangeSet(t *testing.T) {
 	type num []uint32
 	tests := []struct {
 		num num
-		rng Seq
+		rng imap.NumRange
 		set string
 		out string
 	}{
-		{num{5}, Seq{1, 3}, "1:2,5,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
-		{num{5}, Seq{3, 1}, "2:3,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
+		{num{5}, imap.NumRange{1, 3}, "1:2,5,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
+		{num{5}, imap.NumRange{3, 1}, "2:3,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
 
-		{num{15}, Seq{17, 0}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
-		{num{15}, Seq{0, 17}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
+		{num{15}, imap.NumRange{17, 0}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
+		{num{15}, imap.NumRange{0, 17}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
 
-		{num{1, 3, 5, 7, 9, 11, 0}, Seq{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
-		{num{5, 1, 7, 3, 9, 0, 11}, Seq{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
-		{num{5, 1, 7, 3, 9, 0, 11}, Seq{13, 8}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
+		{num{1, 3, 5, 7, 9, 11, 0}, imap.NumRange{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
+		{num{5, 1, 7, 3, 9, 0, 11}, imap.NumRange{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
+		{num{5, 1, 7, 3, 9, 0, 11}, imap.NumRange{13, 8}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
 	}
 	for _, test := range tests {
-		other, _ := ParseSeqSet(test.set)
+		other, _ := ParseNumSet(test.set)
 
-		var s SeqSet
+		var s imap.NumSet
 		s.AddNum(test.num...)
-		checkSeqSet(s, t)
+		checkNumSet(s, t)
 		s.AddRange(test.rng.Start, test.rng.Stop)
-		checkSeqSet(s, t)
+		checkNumSet(s, t)
 		s.AddSet(other)
-		checkSeqSet(s, t)
+		checkNumSet(s, t)
 
 		if out := s.String(); out != test.out {
 			t.Errorf("(%v + %v + %q).String() expected %q; got %q", test.num, test.rng, test.set, test.out, out)