aboutsummaryrefslogblamecommitdiffstats
path: root/worker/imap/idler.go
blob: 65ba6b9b22d29e2b7d3b6228f65b03797615533f (plain) (tree)





























































                                                                               











                                    
                         

                         
                                            
 

                                                    






                                                                  
                                                  






                                                                 
                                                   
                                             
                                                                               
                         
                   
 
                           
                                                           
                





                                                      

                         





                                                 
                                                                   













                                                                              
                           

                                                           
                










                                                      








                                                                           
                 



                                            


           

                                                      
                                                                                               
 
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) setIdleing(v bool) {
	i.Lock()
	defer i.Unlock()
	i.idleing = v
}

func (i *idler) isIdleing() bool {
	i.Lock()
	defer i.Unlock()
	return i.idleing
}

func (i *idler) Start() {
	switch {
	case 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.setIdleing(true)
				i.log("=>(idle)")
				now := time.Now()
				err := i.client.Idle(i.stop,
					&client.IdleOptions{
						LogoutTimeout: 0,
						PollInterval:  0,
					})
				i.setIdleing(false)
				i.done <- err
				i.log("elapsed idle time: %v", time.Since(now))
			}
		}()

	case i.isWaiting():
		i.log("not started: wait for idle to exit")
	default:
		i.log("not started: client not ready")
	}
}

func (i *idler) Stop() error {
	var reterr error
	switch {
	case 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
		}
	case i.isWaiting():
		i.log("not stopped: still idleing/hanging")
		reterr = errIdleModeHangs
	default:
		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()
		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()
	}()
}

func (i *idler) log(format string, v ...interface{}) {
	msg := fmt.Sprintf(format, v...)
	logging.Debugf("idler (%p) [idle:%t,wait:%t] %s", i, i.isIdleing(), i.isWaiting(), msg)
}