Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7e8c5a1
add independent capabilities
mercata Jun 6, 2025
b6e4b9c
imapserver: add SORT and SORT=DISPLAY capabilities
mercata Jun 7, 2025
08495b9
imapserver: add ESORT capability
mercata Jun 7, 2025
384cff2
use a list instead of a struct
mercata Jun 7, 2025
6c983fa
capability ID required separate parsing, adding it now
mercata Jun 7, 2025
025cfbc
fix: ensure response encoder is properly finalized in handleID
mercata Jun 8, 2025
207d3ae
Merge branch 'v2' into capabilities
dejanstrbac Jun 9, 2025
cd0603c
Merge branch 'v2' into sort
dejanstrbac Jun 9, 2025
94bb68a
moving CapChildren to own PR
mercata Jun 9, 2025
200aa88
Merge branch 'v2' into capabilities
dejanstrbac Jun 11, 2025
bb05d23
mkae CapID applicable to both rev1 and rev2
mercata Jun 11, 2025
100d053
Merge branch 'v2' into capabilities
dejanstrbac Jun 11, 2025
fd118b6
Merge branch 'v2' into sort
dejanstrbac Jun 11, 2025
44d415f
moved logic for sortdata to library from memserver
mercata Jun 11, 2025
d5a432d
Merge branch 'v2' into sort
dejanstrbac Jun 11, 2025
6ed205a
Add CONDSTORE extension support
mercata Jun 6, 2025
8a69868
Add CONDSTORE extension support
mercata Jun 6, 2025
aa89543
resolve merge error and fmt
mercata Jun 11, 2025
43bd92a
removing merge conflicts leftovers
mercata Jun 11, 2025
1a045eb
Merge branch 'v2' into sort
dejanstrbac Jun 11, 2025
884d373
Merge branch 'v2' into capabilities
dejanstrbac Jun 11, 2025
dd9e5b0
Merge branch 'v2' into capabilities
dejanstrbac Sep 21, 2025
bff9940
Merge branch 'v2' into sort
dejanstrbac Sep 21, 2025
0e418b8
Extend ID command on both client and server for ID forwarding in Dovecot
mercata Sep 21, 2025
f2fdf37
corrected ESORT handling as per RFC5267
mercata Sep 21, 2025
b625cbe
Merge branch 'v2' into sort
dejanstrbac Sep 21, 2025
0446b74
Merge branch 'v2' into capabilities
dejanstrbac Sep 21, 2025
6793216
According to RFC 4731, a server must ignore any unrecognized RETURN o…
mercata Sep 21, 2025
649289c
Merge branch 'v2' into v2search
dejanstrbac Sep 21, 2025
e38020d
Merge branch 'v2' into condstore
dejanstrbac Sep 21, 2025
1f423a7
updates of requested changes
mercata Sep 21, 2025
a1954be
Missing CONDSTORE, Search fixes and QRESYNC support
mercata Sep 21, 2025
239a2ae
update test to support QRESYNC
mercata Sep 21, 2025
8539f83
send CONDSTORE only if advertised
mercata Sep 21, 2025
30e5af0
Refactor setCaps() and Caps() for race condition
mercata Sep 21, 2025
943e8f6
feat(condstore): add CONDSTORE to ENABLE
mercata Sep 21, 2025
d1d721c
enable condstore in client tests
mercata Sep 21, 2025
cf33794
CHANGEDSINCE is only in Fetch, not Search according to fc7162
mercata Sep 21, 2025
55b3fb2
goftm
mercata Sep 21, 2025
a79d4e5
permanentflags => flags
mercata Sep 21, 2025
01a2948
Merge branch 'v2' into imapclientflags
dejanstrbac Sep 21, 2025
8a3cc0a
Allow empty lines / commands without breaking connection
mercata Sep 21, 2025
409fe31
Merge branch 'v2' into emptycmd
dejanstrbac Sep 21, 2025
4d6ceb6
Consistent and context aware utf handling
mercata Sep 21, 2025
68a2840
Merge branch 'v2' into utf
dejanstrbac Sep 21, 2025
f78e973
Go fmt
mercata Sep 22, 2025
50b99b2
Merge pull request #7 from migadu/capabilities
dejanstrbac Sep 23, 2025
a0ebec9
Merge pull request #2 from dejanstrbac/utf
dejanstrbac Sep 23, 2025
59c6420
Merge pull request #3 from dejanstrbac/emptycmd
dejanstrbac Sep 23, 2025
a746919
Merge pull request #4 from dejanstrbac/imapclientflags
dejanstrbac Sep 23, 2025
e03666f
Merge pull request #5 from dejanstrbac/v2search
dejanstrbac Sep 23, 2025
f8689b4
Merge branch 'sora' into sort
dejanstrbac Sep 23, 2025
1506e71
Merge pull request #6 from migadu/sort
dejanstrbac Sep 23, 2025
b75e766
Merge branch 'sora' into condstore
dejanstrbac Sep 23, 2025
ab7be97
Merge branch 'v2' into condstore
dejanstrbac Sep 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@ func (set CapSet) Has(c Cap) bool {
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
return true
}
if c == CapCondStore && set.has(CapQResync) {

// IMAP4rev2 implies QRESYNC, which in turn implies CONDSTORE.
isQResync := set.has(CapQResync) || set.has(CapIMAP4rev2)
if c == CapQResync && isQResync {
return true
}
if c == CapCondStore && isQResync {
return true
}
if c == CapUTF8Accept && set.has(CapUTF8Only) {
Expand Down
1 change: 1 addition & 0 deletions fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type FetchOptions struct {
ModSeq bool // requires CONDSTORE

ChangedSince uint64 // requires CONDSTORE
Vanished bool // requires QRESYNC, only valid for UID FETCH
}

// FetchItemBodyStructure contains FETCH options for the body structure.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ go 1.18
require (
github.com/emersion/go-message v0.18.1
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
golang.org/x/text v0.14.0
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down
4 changes: 4 additions & 0 deletions id.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ type IDData struct {
Command string
Arguments string
Environment string

// Raw contains all raw key-value pairs. Standard keys are also present
// in this map. Keys are case-insensitive and are normalized to lowercase.
Raw map[string]string
}
84 changes: 45 additions & 39 deletions imapclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,15 @@ type Client struct {
decCh chan struct{}
decErr error

mutex sync.Mutex
state imap.ConnState
caps imap.CapSet
enabled imap.CapSet
pendingCapCh chan struct{}
mailbox *SelectedMailbox
cmdTag uint64
pendingCmds []command
contReqs []continuationRequest
closed bool
mutex sync.Mutex
state imap.ConnState
caps imap.CapSet
enabled imap.CapSet
mailbox *SelectedMailbox
cmdTag uint64
pendingCmds []command
contReqs []continuationRequest
closed bool
}

// New creates a new IMAP client.
Expand Down Expand Up @@ -319,58 +318,35 @@ func (c *Client) Caps() imap.CapSet {

c.mutex.Lock()
caps := c.caps
capCh := c.pendingCapCh
c.mutex.Unlock()

if caps != nil {
return caps
}

if capCh == nil {
capCmd := c.Capability()
capCh := make(chan struct{})
go func() {
capCmd.Wait()
close(capCh)
}()
c.mutex.Lock()
c.pendingCapCh = capCh
c.mutex.Unlock()
}

timer := time.NewTimer(respReadTimeout)
defer timer.Stop()
select {
case <-timer.C:
capCmd := c.Capability()
caps, err := capCmd.Wait()
if err != nil {
return nil
case <-capCh:
// ok
}

// TODO: this is racy if caps are reset before we get the reply
c.mutex.Lock()
defer c.mutex.Unlock()
return c.caps
return caps
}

func (c *Client) setCaps(caps imap.CapSet) {
// If the capabilities are being reset, request the updated capabilities
// from the server
var capCh chan struct{}
if caps == nil {
capCh = make(chan struct{})

// We need to send the CAPABILITY command in a separate goroutine:
// setCaps might be called with Client.encMutex locked
go func() {
c.Capability().Wait()
close(capCh)
}()
}

c.mutex.Lock()
c.caps = caps
c.pendingCapCh = capCh
quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept)
c.dec.QuotedUTF8 = quotedUTF8
c.mutex.Unlock()
}

Expand Down Expand Up @@ -973,6 +949,11 @@ func (c *Client) readResponseData(typ string) error {
return c.handleFetch(num)
case "EXPUNGE":
return c.handleExpunge(num)
case "VANISHED":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
return c.handleVanished()
case "SEARCH":
return c.handleSearch()
case "ESEARCH":
Expand Down Expand Up @@ -1013,6 +994,28 @@ func (c *Client) readResponseData(typ string) error {
return nil
}

func (c *Client) handleVanished() error {
var data imap.VanishedData
isParen := c.dec.Special('(')
if isParen {
var tag string
if !c.dec.ExpectAtom(&tag) || !c.dec.ExpectSpecial(')') {
return c.dec.Err()
}
data.Earlier = strings.ToUpper(tag) == "EARLIER"
}

if !c.dec.ExpectSP() || !c.dec.ExpectUIDSet(&data.UIDs) {
return c.dec.Err()
}

if handler := c.options.unilateralDataHandler().Vanished; handler != nil {
handler(&data)
}

return nil
}

// WaitGreeting waits for the server's initial greeting.
func (c *Client) WaitGreeting() error {
select {
Expand Down Expand Up @@ -1185,6 +1188,9 @@ type UnilateralDataHandler struct {
Mailbox func(data *UnilateralDataMailbox)
Fetch func(msg *FetchMessageData)

// requires ENABLE QRESYNC
Vanished func(data *imap.VanishedData)

// requires ENABLE METADATA or ENABLE SERVER-METADATA
Metadata func(mailbox string, entries []string)
}
Expand Down
11 changes: 11 additions & 0 deletions imapclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
imap.CapCondStore: {},
imap.CapQResync: {},
},
})

Expand Down Expand Up @@ -178,6 +180,15 @@ func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient
}
}

// Enable CONDSTORE for Dovecot tests (required for CONDSTORE features)
if useDovecot && initialState >= imap.ConnStateAuthenticated {
if client.Caps().Has(imap.CapCondStore) {
if _, err := client.Enable(imap.CapCondStore).Wait(); err != nil {
t.Logf("Failed to enable CONDSTORE: %v", err)
}
}
}

// Turn on debug logs after we're done initializing the test
debugWriter.Swap(os.Stderr)

Expand Down
Loading