diff options
Diffstat (limited to 'worker/imap/idler.go')
-rw-r--r-- | worker/imap/idler.go | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/worker/imap/idler.go b/worker/imap/idler.go new file mode 100644 index 00000000..e9aecfd9 --- /dev/null +++ b/worker/imap/idler.go @@ -0,0 +1,149 @@ +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 + 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() + 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 ideling time:", 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:", 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:", err) + } + i.setWaiting(false) + i.stop = make(chan struct{}) + i.log("restart") + i.Start() + return + } + }() +} + +func (i *idler) log(args ...interface{}) { + header := fmt.Sprintf("idler (%p) [idle:%t,wait:%t]", i, i.idleing, i.waiting) + i.worker.Logger.Println(append([]interface{}{header}, args...)...) +} |