package imap
import (
"fmt"
"sync"
"time"
"git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/worker/types"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
)
var (
errIdleTimeout = fmt.Errorf("idle timeout")
errIdleModeHangs = fmt.Errorf("idle mode hangs; waiting to reconnect")
)
// idler manages the idle mode of the imap server. Enter idle mode if there's
// no other task and leave idle mode when a new task arrives. Idle mode is only
// used when the client is ready and connected. After a connection loss, make
// sure that idling returns gracefully and the worker remains responsive.
type idler struct {
sync.Mutex
config imapConfig
client *imapClient
worker *types.Worker
last time.Time
stop chan struct{}
done chan error
waiting bool
idleing bool
}
func newIdler(cfg imapConfig, w *types.Worker) *idler {
return &idler{config: cfg, worker: w, done: make(chan error)}
}
func (i *idler) SetClient(c *imapClient) {
i.Lock()
i.client = c
i.Unlock()
}
func (i *idler) setWaiting(wait bool) {
i.Lock()
i.waiting = wait
i.Unlock()
}
func (i *idler) isWaiting() bool {
i.Lock()
defer i.Unlock()
return i.waiting
}
func (i *idler) isReady() bool {
i.Lock()
defer i.Unlock()
return (!i.waiting && i.client != nil &&
i.client.State() == imap.SelectedState)
}
func (i *idler) Start() {
if i.isReady() {
i.stop = make(chan struct{})
go func() {
defer logging.PanicHandler()
select {
case <-i.stop:
// debounce idle
i.log("=>(idle) [debounce]")
i.done <- nil
case <-time.After(i.config.idle_debounce):
// enter idle mode
i.idleing = true
i.log("=>(idle)")
now := time.Now()
err := i.client.Idle(i.stop,
&client.IdleOptions{
LogoutTimeout: 0,
PollInterval: 0,
})
i.idleing = false
i.done <- err
i.log("elapsed idle time: %v", time.Since(now))
}
}()
} else if i.isWaiting() {
i.log("not started: wait for idle to exit")
} else {
i.log("not started: client not ready")
}
}
func (i *idler) Stop() error {
var reterr error
if i.isReady() {
close(i.stop)
select {
case err := <-i.done:
if err == nil {
i.log("<=(idle)")
} else {
i.log("<=(idle) with err: %v", err)
}
reterr = nil
case <-time.After(i.config.idle_timeout):
i.log("idle err (timeout); waiting in background")
i.log("disconnect done->")
i.worker.PostMessage(&types.Done{
Message: types.RespondTo(&types.Disconnect{}),
}, nil)
i.waitOnIdle()
reterr = errIdleTimeout
}
} else if i.isWaiting() {
i.log("not stopped: still idleing/hanging")
reterr = errIdleModeHangs
} else {
i.log("not stopped: client not ready")
reterr = nil
}
return reterr
}
func (i *idler) waitOnIdle() {
i.setWaiting(true)
i.log("wait for idle in background")
go func() {
defer logging.PanicHandler()
select {
case err := <-i.done:
if err == nil {
i.log("<=(idle) waited")
i.log("connect done->")
i.worker.PostMessage(&types.Done{
Message: types.RespondTo(&types.Connect{}),
}, nil)
} else {
i.log("<=(idle) waited; with err: %v", err)
}
i.setWaiting(false)
i.stop = make(chan struct{})
i.log("restart")
i.Start()
return
}
}()
}
func (i *idler) log(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
logging.Debugf("idler (%p) [idle:%t,wait:%t] %s", i, i.idleing, i.waiting, msg)
}