Skip to content

Commit 9f9bf90

Browse files
committed
imapclient: restart IDLE automatically
1 parent 38838c5 commit 9f9bf90

File tree

1 file changed

+100
-11
lines changed

1 file changed

+100
-11
lines changed

imapclient/idle.go

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,33 @@ package imapclient
22

33
import (
44
"fmt"
5+
"sync/atomic"
6+
"time"
57
)
68

9+
const idleRestartInterval = 28 * time.Minute
10+
711
// Idle sends an IDLE command.
812
//
913
// Unlike other commands, this method blocks until the server acknowledges it.
1014
// On success, the IDLE command is running and other commands cannot be sent.
1115
// The caller must invoke IdleCommand.Close to stop IDLE and unblock the
1216
// client.
1317
//
14-
// This command requires support for IMAP4rev2 or the IDLE extension.
18+
// This command requires support for IMAP4rev2 or the IDLE extension. The IDLE
19+
// command is restarted automatically to avoid getting disconnected due to
20+
// inactivity timeouts.
1521
func (c *Client) Idle() (*IdleCommand, error) {
16-
cmd := &IdleCommand{}
17-
contReq := c.registerContReq(cmd)
18-
cmd.enc = c.beginCommand("IDLE", cmd)
19-
cmd.enc.flush()
20-
21-
_, err := contReq.Wait()
22+
child, err := c.idle()
2223
if err != nil {
23-
cmd.enc.end()
2424
return nil, err
2525
}
2626

27+
cmd := &IdleCommand{
28+
stop: make(chan struct{}),
29+
done: make(chan struct{}),
30+
}
31+
go cmd.run(c, child)
2732
return cmd, nil
2833
}
2934

@@ -34,6 +39,90 @@ func (c *Client) Idle() (*IdleCommand, error) {
3439
//
3540
// Close must be called to stop the IDLE command.
3641
type IdleCommand struct {
42+
stopped atomic.Bool
43+
stop chan struct{}
44+
done chan struct{}
45+
46+
err error
47+
lastChild *idleCommand
48+
}
49+
50+
func (cmd *IdleCommand) run(c *Client, child *idleCommand) {
51+
defer close(cmd.done)
52+
53+
timer := time.NewTimer(idleRestartInterval)
54+
defer timer.Stop()
55+
56+
defer func() {
57+
if child != nil {
58+
if err := child.Close(); err != nil && cmd.err == nil {
59+
cmd.err = err
60+
}
61+
}
62+
}()
63+
64+
for {
65+
select {
66+
case <-timer.C:
67+
timer.Reset(idleRestartInterval)
68+
69+
if cmd.err = child.Close(); cmd.err != nil {
70+
return
71+
}
72+
if child, cmd.err = c.idle(); cmd.err != nil {
73+
return
74+
}
75+
case <-cmd.stop:
76+
cmd.lastChild = child
77+
return
78+
}
79+
}
80+
}
81+
82+
// Close stops the IDLE command.
83+
//
84+
// This method blocks until the command to stop IDLE is written, but doesn't
85+
// wait for the server to respond. Callers can use Wait for this purpose.
86+
func (cmd *IdleCommand) Close() error {
87+
if cmd.stopped.Swap(true) {
88+
return fmt.Errorf("imapclient: IDLE already closed")
89+
}
90+
close(cmd.stop)
91+
<-cmd.done
92+
return cmd.err
93+
}
94+
95+
// Wait blocks until the IDLE command has completed.
96+
//
97+
// Wait can only be called after Close.
98+
func (cmd *IdleCommand) Wait() error {
99+
if !cmd.stopped.Load() {
100+
return fmt.Errorf("imapclient: IdleCommand.Close must be called before Wait")
101+
}
102+
<-cmd.done
103+
if cmd.err != nil {
104+
return cmd.err
105+
}
106+
return cmd.lastChild.Wait()
107+
}
108+
109+
func (c *Client) idle() (*idleCommand, error) {
110+
cmd := &idleCommand{}
111+
contReq := c.registerContReq(cmd)
112+
cmd.enc = c.beginCommand("IDLE", cmd)
113+
cmd.enc.flush()
114+
115+
_, err := contReq.Wait()
116+
if err != nil {
117+
cmd.enc.end()
118+
return nil, err
119+
}
120+
121+
return cmd, nil
122+
}
123+
124+
// idleCommand represents a singular IDLE command, without the restart logic.
125+
type idleCommand struct {
37126
cmd
38127
enc *commandEncoder
39128
}
@@ -42,7 +131,7 @@ type IdleCommand struct {
42131
//
43132
// This method blocks until the command to stop IDLE is written, but doesn't
44133
// wait for the server to respond. Callers can use Wait for this purpose.
45-
func (cmd *IdleCommand) Close() error {
134+
func (cmd *idleCommand) Close() error {
46135
if cmd.err != nil {
47136
return cmd.err
48137
}
@@ -62,9 +151,9 @@ func (cmd *IdleCommand) Close() error {
62151
// Wait blocks until the IDLE command has completed.
63152
//
64153
// Wait can only be called after Close.
65-
func (cmd *IdleCommand) Wait() error {
154+
func (cmd *idleCommand) Wait() error {
66155
if cmd.enc != nil {
67-
return fmt.Errorf("imapclient: IdleCommand.Close must be called before Wait")
156+
panic("imapclient: idleCommand.Close must be called before Wait")
68157
}
69158
return cmd.cmd.Wait()
70159
}

0 commit comments

Comments
 (0)