Skip to content

Commit ac3f8e1

Browse files
committed
Merge support for IDLE extension
1 parent 231c001 commit ac3f8e1

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

client/cmd_auth.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,105 @@ func (c *Client) Enable(caps []string) ([]string, error) {
276276
return res.Caps, status.Err()
277277
}
278278
}
279+
280+
func (c *Client) idle(stop <-chan struct{}) error {
281+
cmd := &commands.Idle{}
282+
283+
res := &responses.Idle{
284+
Stop: stop,
285+
RepliesCh: make(chan []byte, 10),
286+
}
287+
288+
if status, err := c.Execute(cmd, res); err != nil {
289+
return err
290+
} else {
291+
return status.Err()
292+
}
293+
}
294+
295+
// IdleOptions holds options for Client.Idle.
296+
type IdleOptions struct {
297+
// LogoutTimeout is used to avoid being logged out by the server when
298+
// idling. Each LogoutTimeout, the IDLE command is restarted. If set to
299+
// zero, a default is used. If negative, this behavior is disabled.
300+
LogoutTimeout time.Duration
301+
// Poll interval when the server doesn't support IDLE. If zero, a default
302+
// is used. If negative, polling is always disabled.
303+
PollInterval time.Duration
304+
}
305+
306+
// Idle indicates to the server that the client is ready to receive unsolicited
307+
// mailbox update messages. When the client wants to send commands again, it
308+
// must first close stop.
309+
//
310+
// If the server doesn't support IDLE, go-imap falls back to polling.
311+
func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error {
312+
if ok, err := c.Support("IDLE"); err != nil {
313+
return err
314+
} else if !ok {
315+
return c.idleFallback(stop, opts)
316+
}
317+
318+
logoutTimeout := 25 * time.Minute
319+
if opts != nil {
320+
if opts.LogoutTimeout > 0 {
321+
logoutTimeout = opts.LogoutTimeout
322+
} else if opts.LogoutTimeout < 0 {
323+
return c.idle(stop)
324+
}
325+
}
326+
327+
t := time.NewTicker(logoutTimeout)
328+
defer t.Stop()
329+
330+
for {
331+
stopOrRestart := make(chan struct{})
332+
done := make(chan error, 1)
333+
go func() {
334+
done <- c.idle(stopOrRestart)
335+
}()
336+
337+
select {
338+
case <-t.C:
339+
close(stopOrRestart)
340+
if err := <-done; err != nil {
341+
return err
342+
}
343+
case <-stop:
344+
close(stopOrRestart)
345+
return <-done
346+
case err := <-done:
347+
close(stopOrRestart)
348+
if err != nil {
349+
return err
350+
}
351+
}
352+
}
353+
}
354+
355+
func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error {
356+
pollInterval := time.Minute
357+
if opts != nil {
358+
if opts.PollInterval > 0 {
359+
pollInterval = opts.PollInterval
360+
} else if opts.PollInterval < 0 {
361+
return ErrExtensionUnsupported
362+
}
363+
}
364+
365+
t := time.NewTicker(pollInterval)
366+
defer t.Stop()
367+
368+
for {
369+
select {
370+
case <-t.C:
371+
if err := c.Noop(); err != nil {
372+
return err
373+
}
374+
case <-stop:
375+
return nil
376+
case <-c.LoggedOut():
377+
return errors.New("disconnected while idling")
378+
}
379+
}
380+
}

client/example_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,43 @@ func ExampleClient_Search() {
281281

282282
log.Println("Done!")
283283
}
284+
285+
func ExampleClient_Idle() {
286+
// Let's assume c is a client
287+
var c *client.Client
288+
289+
// Select a mailbox
290+
if _, err := c.Select("INBOX", false); err != nil {
291+
log.Fatal(err)
292+
}
293+
294+
// Create a channel to receive mailbox updates
295+
updates := make(chan client.Update)
296+
c.Updates = updates
297+
298+
// Start idling
299+
stopped := false
300+
stop := make(chan struct{})
301+
done := make(chan error, 1)
302+
go func() {
303+
done <- c.Idle(stop, nil)
304+
}()
305+
306+
// Listen for updates
307+
for {
308+
select {
309+
case update := <-updates:
310+
log.Println("New update:", update)
311+
if !stopped {
312+
close(stop)
313+
stopped = true
314+
}
315+
case err := <-done:
316+
if err != nil {
317+
log.Fatal(err)
318+
}
319+
log.Println("Not idling anymore")
320+
return
321+
}
322+
}
323+
}

server/cmd_auth.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package server
22

33
import (
4+
"bufio"
45
"errors"
6+
"strings"
57

68
"github.com/emersion/go-imap"
79
"github.com/emersion/go-imap/backend"
@@ -297,3 +299,26 @@ func (cmd *Unselect) Handle(conn Conn) error {
297299
ctx.MailboxReadOnly = false
298300
return nil
299301
}
302+
303+
type Idle struct {
304+
commands.Idle
305+
}
306+
307+
func (cmd *Idle) Handle(conn Conn) error {
308+
cont := &imap.ContinuationReq{Info: "idling"}
309+
if err := conn.WriteResp(cont); err != nil {
310+
return err
311+
}
312+
313+
// Wait for DONE
314+
scanner := bufio.NewScanner(conn)
315+
scanner.Scan()
316+
if err := scanner.Err(); err != nil {
317+
return err
318+
}
319+
320+
if strings.ToUpper(scanner.Text()) != "DONE" {
321+
return errors.New("Expected DONE")
322+
}
323+
return nil
324+
}

server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ func New(bkd backend.Backend) *Server {
177177
"STATUS": func() Handler { return &Status{} },
178178
"APPEND": func() Handler { return &Append{} },
179179
"UNSELECT": func() Handler { return &Unselect{} },
180+
"IDLE": func() Handler { return &Idle{} },
180181

181182
"CHECK": func() Handler { return &Check{} },
182183
"CLOSE": func() Handler { return &Close{} },

0 commit comments

Comments
 (0)