Skip to content

Commit

Permalink
Merge support for IDLE extension
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion committed Sep 7, 2021
1 parent 231c001 commit ac3f8e1
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 0 deletions.
102 changes: 102 additions & 0 deletions client/cmd_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,105 @@ func (c *Client) Enable(caps []string) ([]string, error) {
return res.Caps, status.Err()
}
}

func (c *Client) idle(stop <-chan struct{}) error {
cmd := &commands.Idle{}

res := &responses.Idle{
Stop: stop,
RepliesCh: make(chan []byte, 10),
}

if status, err := c.Execute(cmd, res); err != nil {
return err
} else {
return status.Err()
}
}

// IdleOptions holds options for Client.Idle.
type IdleOptions struct {
// LogoutTimeout is used to avoid being logged out by the server when
// idling. Each LogoutTimeout, the IDLE command is restarted. If set to
// zero, a default is used. If negative, this behavior is disabled.
LogoutTimeout time.Duration
// Poll interval when the server doesn't support IDLE. If zero, a default
// is used. If negative, polling is always disabled.
PollInterval time.Duration
}

// Idle indicates to the server that the client is ready to receive unsolicited
// mailbox update messages. When the client wants to send commands again, it
// must first close stop.
//
// If the server doesn't support IDLE, go-imap falls back to polling.
func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error {
if ok, err := c.Support("IDLE"); err != nil {
return err
} else if !ok {
return c.idleFallback(stop, opts)
}

logoutTimeout := 25 * time.Minute
if opts != nil {
if opts.LogoutTimeout > 0 {
logoutTimeout = opts.LogoutTimeout
} else if opts.LogoutTimeout < 0 {
return c.idle(stop)
}
}

t := time.NewTicker(logoutTimeout)
defer t.Stop()

for {
stopOrRestart := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- c.idle(stopOrRestart)
}()

select {
case <-t.C:
close(stopOrRestart)
if err := <-done; err != nil {
return err
}
case <-stop:
close(stopOrRestart)
return <-done
case err := <-done:
close(stopOrRestart)
if err != nil {
return err
}
}
}
}

func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error {
pollInterval := time.Minute
if opts != nil {
if opts.PollInterval > 0 {
pollInterval = opts.PollInterval
} else if opts.PollInterval < 0 {
return ErrExtensionUnsupported
}
}

t := time.NewTicker(pollInterval)
defer t.Stop()

for {
select {
case <-t.C:
if err := c.Noop(); err != nil {
return err
}
case <-stop:
return nil
case <-c.LoggedOut():
return errors.New("disconnected while idling")
}
}
}
40 changes: 40 additions & 0 deletions client/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,43 @@ func ExampleClient_Search() {

log.Println("Done!")
}

func ExampleClient_Idle() {
// Let's assume c is a client
var c *client.Client

// Select a mailbox
if _, err := c.Select("INBOX", false); err != nil {
log.Fatal(err)
}

// Create a channel to receive mailbox updates
updates := make(chan client.Update)
c.Updates = updates

// Start idling
stopped := false
stop := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- c.Idle(stop, nil)
}()

// Listen for updates
for {
select {
case update := <-updates:
log.Println("New update:", update)
if !stopped {
close(stop)
stopped = true
}
case err := <-done:
if err != nil {
log.Fatal(err)
}
log.Println("Not idling anymore")
return
}
}
}
25 changes: 25 additions & 0 deletions server/cmd_auth.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package server

import (
"bufio"
"errors"
"strings"

"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
Expand Down Expand Up @@ -297,3 +299,26 @@ func (cmd *Unselect) Handle(conn Conn) error {
ctx.MailboxReadOnly = false
return nil
}

type Idle struct {
commands.Idle
}

func (cmd *Idle) Handle(conn Conn) error {
cont := &imap.ContinuationReq{Info: "idling"}
if err := conn.WriteResp(cont); err != nil {
return err
}

// Wait for DONE
scanner := bufio.NewScanner(conn)
scanner.Scan()
if err := scanner.Err(); err != nil {
return err
}

if strings.ToUpper(scanner.Text()) != "DONE" {
return errors.New("Expected DONE")
}
return nil
}
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func New(bkd backend.Backend) *Server {
"STATUS": func() Handler { return &Status{} },
"APPEND": func() Handler { return &Append{} },
"UNSELECT": func() Handler { return &Unselect{} },
"IDLE": func() Handler { return &Idle{} },

"CHECK": func() Handler { return &Check{} },
"CLOSE": func() Handler { return &Close{} },
Expand Down

0 comments on commit ac3f8e1

Please sign in to comment.