aboutsummaryrefslogtreecommitdiffstats
path: root/worker/imap/idler.go
diff options
context:
space:
mode:
Diffstat (limited to 'worker/imap/idler.go')
-rw-r--r--worker/imap/idler.go149
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...)...)
+}