Skip to content

Commit adc2995

Browse files
committed
imapclient: implement Client.Closed
When using a context to close connections, the goroutine blocking on the context to close the connection will keep a reference to the connection, so it is never free'd from memory. For long running processes, this means that we accumulate connections stale indefinitely. Implement a Client.Closed() to be used by clients to monitor when a connection is closed. This likely has other use-cases.
1 parent 17771fb commit adc2995

File tree

3 files changed

+64
-0
lines changed

3 files changed

+64
-0
lines changed

imapclient/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,11 @@ func (c *Client) Mailbox() *SelectedMailbox {
385385
return c.mailbox
386386
}
387387

388+
// Closed returns a channel that is closed when the connection is closed.
389+
func (c *Client) Closed() <-chan struct{} {
390+
return c.decCh
391+
}
392+
388393
// Close immediately closes the connection.
389394
func (c *Client) Close() error {
390395
c.mutex.Lock()

imapclient/connection_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package imapclient_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/emersion/go-imap/v2"
8+
)
9+
10+
// TestClient_Closed tests that the Closed() channel is closed when the
11+
// connection is explicitly closed via Close().
12+
func TestClient_Closed(t *testing.T) {
13+
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
14+
defer server.Close()
15+
16+
closedCh := client.Closed()
17+
if closedCh == nil {
18+
t.Fatal("Closed() returned nil channel")
19+
}
20+
21+
select {
22+
case <-closedCh:
23+
t.Fatal("Closed() channel closed before calling Close()")
24+
default: // Expected
25+
}
26+
27+
if err := client.Close(); err != nil {
28+
t.Fatalf("Close() = %v", err)
29+
}
30+
31+
select {
32+
case <-closedCh:
33+
t.Log("Closed() channel properly closed after Close()")
34+
case <-time.After(2 * time.Second):
35+
t.Fatal("Closed() channel not closed after Close()")
36+
}
37+
}

imapclient/example_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,25 @@ func ExampleClient_Authenticate_oauth() {
378378
log.Fatalf("authentication failed: %v", err)
379379
}
380380
}
381+
382+
func ExampleClient_Closed() {
383+
c, err := imapclient.DialTLS("mail.example.org:993", nil)
384+
if err != nil {
385+
log.Fatalf("failed to dial IMAP server: %v", err)
386+
}
387+
defer c.Close()
388+
389+
if err := c.Login("root", "asdf").Wait(); err != nil {
390+
log.Fatalf("failed to login: %v", err)
391+
}
392+
393+
// Monitor the connection in a separate goroutine.
394+
go func() {
395+
<-c.Closed()
396+
log.Println("Connection has been closed")
397+
}()
398+
399+
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
400+
log.Fatalf("failed to select INBOX: %v", err)
401+
}
402+
}

0 commit comments

Comments
 (0)