imap: manage idle mode with an idler

Untangle the idle functionality from the message handling routine. Wait
for the idle mode to properly exit every time to ensure a consistent
imap state. Timeout when hanging in idle mode and inform the ui.

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Koni Marti 2022-04-30 01:08:55 +02:00 committed by Robin Jarry
parent 4d75137b20
commit 397a6f267f
3 changed files with 182 additions and 25 deletions

View file

@ -95,5 +95,7 @@ func (w *IMAPWorker) handleConfigure(msg *types.Configure) error {
}
}
w.idler = newIdler(w.config, w.worker)
return nil
}

149
worker/imap/idler.go Normal file
View file

@ -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...)...)
}

View file

@ -14,7 +14,6 @@ import (
"github.com/pkg/errors"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/handlers"
"git.sr.ht/~rjarry/aerc/worker/types"
@ -56,44 +55,43 @@ type IMAPWorker struct {
config imapConfig
client *imapClient
idleStop chan struct{}
idleDone chan error
selected *imap.MailboxStatus
updates chan client.Update
worker *types.Worker
// Map of sequence numbers to UIDs, index 0 is seq number 1
seqMap []uint32
done chan struct{}
autoReconnect bool
retries int
idler *idler
}
func NewIMAPWorker(worker *types.Worker) (types.Backend, error) {
return &IMAPWorker{
idleDone: make(chan error),
updates: make(chan client.Update, 50),
worker: worker,
selected: &imap.MailboxStatus{},
idler: newIdler(imapConfig{}, worker),
}, nil
}
func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
if w.client != nil && w.client.State() == imap.SelectedState {
close(w.idleStop)
if err := <-w.idleDone; err != nil {
w.worker.PostMessage(&types.Error{Error: err}, nil)
}
}
defer func() {
if w.client != nil && w.client.State() == imap.SelectedState {
w.idleStop = make(chan struct{})
go func() {
defer logging.PanicHandler()
func (w *IMAPWorker) newClient(c *client.Client) {
c.Updates = w.updates
w.client = &imapClient{c, sortthread.NewThreadClient(c), sortthread.NewSortClient(c)}
w.idler.SetClient(w.client)
}
w.idleDone <- w.client.Idle(w.idleStop, &client.IdleOptions{LogoutTimeout: 0, PollInterval: 0})
func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
defer func() {
w.idler.Start()
}()
if err := w.idler.Stop(); err != nil {
return err
}
}()
var reterr error // will be returned at the end, needed to support idle
checkConn := func(wait time.Duration) {
time.Sleep(wait)
@ -101,7 +99,10 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
w.startConnectionObserver()
}
var reterr error // will be returned at the end, needed to support idle
// set connection timeout for calls to imap server
if w.client != nil {
w.client.Timeout = w.config.connection_timeout
}
switch msg := msg.(type) {
case *types.Unsupported:
@ -128,8 +129,7 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
w.stopConnectionObserver()
c.Updates = w.updates
w.client = &imapClient{c, sortthread.NewThreadClient(c), sortthread.NewSortClient(c)}
w.newClient(c)
w.startConnectionObserver()
@ -150,8 +150,7 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
w.stopConnectionObserver()
c.Updates = w.updates
w.client = &imapClient{c, sortthread.NewThreadClient(c), sortthread.NewSortClient(c)}
w.newClient(c)
w.startConnectionObserver()
@ -203,6 +202,11 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
reterr = errUnsupported
}
// we don't want idle to timeout, so set timeout to zero
if w.client != nil {
w.client.Timeout = 0
}
return reterr
}
@ -433,6 +437,7 @@ func (w *IMAPWorker) Run() {
select {
case msg := <-w.worker.Actions:
msg = w.worker.ProcessAction(msg)
if err := w.handleMessage(msg); err == errUnsupported {
w.worker.PostMessage(&types.Unsupported{
Message: types.RespondTo(msg),
@ -443,6 +448,7 @@ func (w *IMAPWorker) Run() {
Error: err,
}, nil)
}
case update := <-w.updates:
w.handleImapUpdate(update)
}