mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-04-23 05:10:16 +02:00
Add Directory and Filter fields to DirectoryContents, DirectoryThreaded, SearchResults, MessageInfo, and FullMessage response types. Workers now populate these fields so the UI knows which directory and filter context each response belongs to. This metadata allows the message store to correctly associate incoming messages with their source context, which is essential for the offline worker to cache messages by directory and for proper handling of concurrent operations across multiple folders. Signed-off-by: Robin Jarry <robin@jarry.cc> Reviewed-by: Simon Martin <simon@nasilyan.com>
954 lines
25 KiB
Go
954 lines
25 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib"
|
|
"git.sr.ht/~rjarry/aerc/lib/hooks"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/marker"
|
|
"git.sr.ht/~rjarry/aerc/lib/pama"
|
|
"git.sr.ht/~rjarry/aerc/lib/sort"
|
|
"git.sr.ht/~rjarry/aerc/lib/state"
|
|
"git.sr.ht/~rjarry/aerc/lib/templates"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
"git.sr.ht/~rockorager/vaxis"
|
|
)
|
|
|
|
var _ ProvidesMessages = (*AccountView)(nil)
|
|
|
|
var WorkerMessages = make(chan types.WorkerMessage, 2048)
|
|
|
|
type AccountView struct {
|
|
sync.Mutex
|
|
acct *config.AccountConfig
|
|
dirlist DirectoryLister
|
|
labels []string
|
|
grid *ui.Grid
|
|
tab *ui.Tab
|
|
msglist *MessageList
|
|
worker *types.Worker
|
|
state state.AccountState
|
|
newConn bool // True if this is a first run after a new connection/reconnection
|
|
|
|
split *MessageViewer
|
|
splitSize int
|
|
splitDebounce *time.Timer
|
|
splitDir config.SplitDirection
|
|
splitLoaded bool
|
|
|
|
sidebarHidden bool
|
|
|
|
// Check-mail ticker
|
|
ticker *time.Ticker
|
|
checkingMail bool
|
|
// Indicates whether the account has a new mail: this is a mail that has
|
|
// arrived since the account tab was last focused (if it's currently
|
|
// focused, the flag is not set).
|
|
hasNew bool
|
|
|
|
// Reconnection with exponential backoff
|
|
reconnectRetries int
|
|
}
|
|
|
|
func (acct *AccountView) UiConfig() *config.UIConfig {
|
|
if dirlist := acct.Directories(); dirlist != nil {
|
|
return dirlist.UiConfig("")
|
|
}
|
|
return config.Ui().ForAccount(acct.acct.Name)
|
|
}
|
|
|
|
func NewAccountView(
|
|
acct *config.AccountConfig, deferLoop chan struct{},
|
|
) (*AccountView, error) {
|
|
view := &AccountView{
|
|
acct: acct,
|
|
}
|
|
|
|
worker, err := worker.NewWorker(acct.Source, acct.Name, WorkerMessages)
|
|
if err != nil {
|
|
SetError(fmt.Sprintf("%s: %s", acct.Name, err))
|
|
log.Errorf("%s: %v", acct.Name, err)
|
|
return view, err
|
|
}
|
|
view.worker = worker
|
|
|
|
view.dirlist = NewDirectoryList(acct, worker)
|
|
|
|
view.msglist = NewMessageList(view)
|
|
|
|
view.Configure()
|
|
|
|
go func() {
|
|
defer log.PanicHandler()
|
|
|
|
if deferLoop != nil {
|
|
<-deferLoop
|
|
}
|
|
|
|
worker.Backend.Run()
|
|
}()
|
|
|
|
worker.PostAction(context.TODO(), &types.Configure{Config: acct}, nil)
|
|
worker.PostAction(context.TODO(), &types.Connect{}, nil)
|
|
view.SetStatus(state.ConnectionActivity("Connecting..."))
|
|
if acct.CheckMail.Minutes() > 0 {
|
|
view.CheckMailTimer(acct.CheckMail)
|
|
}
|
|
|
|
return view, nil
|
|
}
|
|
|
|
func (acct *AccountView) Configure() {
|
|
acct.dirlist.OnVirtualNode(func() {
|
|
acct.msglist.SetStore(nil)
|
|
acct.Invalidate()
|
|
})
|
|
acct.buildGrid()
|
|
acct.setTitle()
|
|
|
|
// handle splits
|
|
if acct.split != nil {
|
|
acct.split.Close()
|
|
}
|
|
splitDirection := acct.splitDir
|
|
acct.splitDir = config.SPLIT_NONE
|
|
switch splitDirection {
|
|
case config.SPLIT_HORIZONTAL:
|
|
acct.Split(acct.SplitSize())
|
|
case config.SPLIT_VERTICAL:
|
|
acct.Vsplit(acct.SplitSize())
|
|
}
|
|
}
|
|
|
|
func (acct *AccountView) SetStatus(setters ...state.SetStateFunc) {
|
|
for _, fn := range setters {
|
|
fn(&acct.state, acct.SelectedDirectory())
|
|
}
|
|
acct.UpdateStatus()
|
|
}
|
|
|
|
func (acct *AccountView) UpdateStatus() {
|
|
if acct.isSelected() {
|
|
UpdateStatus()
|
|
}
|
|
}
|
|
|
|
func (acct *AccountView) Select() {
|
|
for i, widget := range aerc.tabs.TabContent.Children() {
|
|
if widget == acct {
|
|
aerc.SelectTabIndex(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (acct *AccountView) PushStatus(status string, expiry time.Duration) {
|
|
PushStatus(fmt.Sprintf("%s: %s", acct.acct.Name, status), expiry)
|
|
}
|
|
|
|
func (acct *AccountView) PushError(err error) {
|
|
PushError(fmt.Sprintf("%s: %v", acct.acct.Name, err))
|
|
}
|
|
|
|
func (acct *AccountView) PushWarning(warning string) {
|
|
PushWarning(fmt.Sprintf("%s: %s", acct.acct.Name, warning))
|
|
}
|
|
|
|
func (acct *AccountView) AccountConfig() *config.AccountConfig {
|
|
return acct.acct
|
|
}
|
|
|
|
func (acct *AccountView) Worker() *types.Worker {
|
|
return acct.worker
|
|
}
|
|
|
|
func (acct *AccountView) Name() string {
|
|
return acct.acct.Name
|
|
}
|
|
|
|
func (acct *AccountView) Invalidate() {
|
|
ui.Invalidate()
|
|
}
|
|
|
|
func (acct *AccountView) Draw(ctx *ui.Context) {
|
|
acct.grid.Draw(ctx)
|
|
}
|
|
|
|
func (acct *AccountView) MouseEvent(localX int, localY int, event vaxis.Event) {
|
|
acct.grid.MouseEvent(localX, localY, event)
|
|
}
|
|
|
|
func (acct *AccountView) Focus(focus bool) {
|
|
// TODO: Unfocus children I guess
|
|
acct.hasNew = false
|
|
acct.setTitle()
|
|
}
|
|
|
|
func (acct *AccountView) Directories() DirectoryLister {
|
|
return acct.dirlist
|
|
}
|
|
|
|
func (acct *AccountView) SetDirectories(d DirectoryLister) {
|
|
if acct.grid != nil {
|
|
acct.grid.ReplaceChild(acct.dirlist, d)
|
|
}
|
|
acct.dirlist = d
|
|
}
|
|
|
|
func (acct *AccountView) Labels() []string {
|
|
return acct.labels
|
|
}
|
|
|
|
func (acct *AccountView) Messages() *MessageList {
|
|
return acct.msglist
|
|
}
|
|
|
|
func (acct *AccountView) Store() *lib.MessageStore {
|
|
if acct.msglist == nil {
|
|
return nil
|
|
}
|
|
return acct.msglist.Store()
|
|
}
|
|
|
|
func (acct *AccountView) SelectedAccount() *AccountView {
|
|
return acct
|
|
}
|
|
|
|
func (acct *AccountView) SelectedDirectory() string {
|
|
return acct.dirlist.Selected()
|
|
}
|
|
|
|
func (acct *AccountView) SelectedMessage() (*models.MessageInfo, error) {
|
|
if acct.msglist == nil || acct.msglist.Store() == nil {
|
|
return nil, errors.New("init in progress")
|
|
}
|
|
if len(acct.msglist.Store().Uids()) == 0 {
|
|
return nil, errors.New("no message selected")
|
|
}
|
|
msg := acct.msglist.Selected()
|
|
if msg == nil {
|
|
return nil, errors.New("message not loaded")
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
func (acct *AccountView) MarkedMessages() ([]models.UID, error) {
|
|
if store := acct.Store(); store != nil {
|
|
return store.Marker().Marked(), nil
|
|
}
|
|
return nil, errors.New("no store available")
|
|
}
|
|
|
|
func (acct *AccountView) SelectedMessagePart() *PartInfo {
|
|
return nil
|
|
}
|
|
|
|
func (acct *AccountView) Terminal() *Terminal {
|
|
if acct.split == nil {
|
|
return nil
|
|
}
|
|
|
|
return acct.split.Terminal()
|
|
}
|
|
|
|
func (acct *AccountView) isSelected() bool {
|
|
return acct == SelectedAccount()
|
|
}
|
|
|
|
func (acct *AccountView) newStore(name string) *lib.MessageStore {
|
|
uiConf := acct.dirlist.UiConfig(name)
|
|
dir := acct.dirlist.Directory(name)
|
|
role := ""
|
|
if dir != nil {
|
|
role = string(dir.Role)
|
|
}
|
|
backend := acct.AccountConfig().Backend
|
|
store := lib.NewMessageStore(acct.worker, name,
|
|
func() *config.UIConfig {
|
|
return config.Ui().
|
|
ForAccount(acct.Name()).
|
|
ForFolder(name)
|
|
},
|
|
func(msg *models.MessageInfo) {
|
|
acct.worker.Debugf("Invoking mail-received hook")
|
|
err := hooks.RunHook(&hooks.MailReceived{
|
|
Account: acct.Name(),
|
|
Backend: backend,
|
|
Folder: name,
|
|
Role: role,
|
|
MsgInfo: msg,
|
|
})
|
|
if err != nil {
|
|
msg := fmt.Sprintf("mail-received hook: %s", err)
|
|
PushError(msg)
|
|
}
|
|
}, func() {
|
|
acct.worker.Debugf("Invoking directory-change hook")
|
|
if uiConf.NewMessageBell {
|
|
aerc.Beep()
|
|
}
|
|
// Set a new message indicator.
|
|
if aerc.SelectedTab() != acct.tab {
|
|
acct.hasNew = true
|
|
}
|
|
}, func() {
|
|
acct.worker.Debugf("Invoking mail-deleted hook")
|
|
err := hooks.RunHook(&hooks.MailDeleted{
|
|
Account: acct.Name(),
|
|
Backend: backend,
|
|
Folder: name,
|
|
Role: role,
|
|
})
|
|
if err != nil {
|
|
msg := fmt.Sprintf("mail-deleted hook: %s", err)
|
|
PushError(msg)
|
|
}
|
|
}, func(dest string) {
|
|
acct.worker.Debugf("Invoking mail-added hook")
|
|
err := hooks.RunHook(&hooks.MailAdded{
|
|
Account: acct.Name(),
|
|
Backend: backend,
|
|
Folder: dest,
|
|
Role: role,
|
|
})
|
|
if err != nil {
|
|
msg := fmt.Sprintf("mail-added hook: %s", err)
|
|
PushError(msg)
|
|
}
|
|
}, func(add []string, remove []string, toggle []string) {
|
|
acct.worker.Debugf("Invoking tag-modified hook")
|
|
err := hooks.RunHook(&hooks.TagModified{
|
|
Account: acct.Name(),
|
|
Backend: backend,
|
|
Add: add,
|
|
Remove: remove,
|
|
Toggle: toggle,
|
|
})
|
|
if err != nil {
|
|
msg := fmt.Sprintf("tag-modified hook: %s", err)
|
|
PushError(msg)
|
|
}
|
|
}, func(flagname string) {
|
|
acct.worker.Debugf("Invoking flag-changed hook")
|
|
err := hooks.RunHook(&hooks.FlagChanged{
|
|
Account: acct.Name(),
|
|
Backend: backend,
|
|
Folder: acct.SelectedDirectory(),
|
|
Role: role,
|
|
FlagName: flagname,
|
|
})
|
|
if err != nil {
|
|
msg := fmt.Sprintf("flag-changed hook: %s", err)
|
|
PushError(msg)
|
|
}
|
|
},
|
|
func(msg *models.MessageInfo) {
|
|
acct.updateSplitView(msg)
|
|
|
|
auto := false
|
|
if c := acct.AccountConfig(); c != nil {
|
|
r, ok := c.Params["pama-auto-switch"]
|
|
if ok {
|
|
if strings.ToLower(r) == "true" {
|
|
auto = true
|
|
}
|
|
}
|
|
}
|
|
if !auto {
|
|
return
|
|
}
|
|
var name string
|
|
if msg != nil && msg.Envelope != nil {
|
|
name = pama.FromSubject(msg.Envelope.Subject)
|
|
}
|
|
pama.DebouncedSwitchProject(name)
|
|
},
|
|
)
|
|
store.Configure(acct.SortCriteria(uiConf))
|
|
store.SetMarker(marker.New(store))
|
|
return store
|
|
}
|
|
|
|
func (acct *AccountView) onMessage(msg types.WorkerMessage) {
|
|
msg = acct.worker.ProcessMessage(msg)
|
|
switch msg := msg.(type) {
|
|
case *types.Done:
|
|
switch resp := msg.InResponseTo().(type) {
|
|
case *types.Connect, *types.Reconnect:
|
|
acct.reconnectRetries = 0
|
|
acct.SetStatus(state.ConnectionActivity("Listing mailboxes..."))
|
|
log.Infof("[%s] connected.", acct.acct.Name)
|
|
acct.SetStatus(state.SetConnected(true))
|
|
log.Tracef("[%s] Listing mailboxes...", acct.acct.Name)
|
|
acct.worker.PostAction(context.TODO(), &types.ListDirectories{}, nil)
|
|
case *types.Disconnect:
|
|
acct.dirlist.ClearList()
|
|
acct.msglist.SetStore(nil)
|
|
log.Infof("[%s] disconnected.", acct.acct.Name)
|
|
acct.SetStatus(state.SetConnected(false))
|
|
case *types.OpenDirectory:
|
|
acct.dirlist.Update(msg)
|
|
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
|
|
// If we've opened this dir before, we can re-render it from
|
|
// memory while we wait for the update and the UI feels
|
|
// snappier. If not, we'll unset the store and show the spinner
|
|
// while we download the UID list.
|
|
acct.msglist.SetStore(store)
|
|
acct.Store().Update(msg.InResponseTo())
|
|
} else {
|
|
acct.msglist.SetStore(nil)
|
|
}
|
|
case *types.CreateDirectory:
|
|
store := acct.newStore(resp.Directory)
|
|
acct.dirlist.SetMsgStore(&models.Directory{
|
|
Name: resp.Directory,
|
|
}, store)
|
|
acct.dirlist.Update(msg)
|
|
case *types.RemoveDirectory:
|
|
acct.dirlist.Update(msg)
|
|
case *types.FetchMessageHeaders:
|
|
if acct.newConn {
|
|
acct.checkMailOnStartup()
|
|
}
|
|
case *types.ListDirectories:
|
|
acct.dirlist.Update(msg)
|
|
if dir := acct.dirlist.Selected(); dir != "" {
|
|
acct.dirlist.Select(dir)
|
|
return
|
|
}
|
|
// Nothing selected, select based on config
|
|
dirs := acct.dirlist.List()
|
|
var dir string
|
|
for _, _dir := range dirs {
|
|
if _dir == acct.acct.Default {
|
|
dir = _dir
|
|
break
|
|
}
|
|
}
|
|
if dir == "" && len(dirs) > 0 {
|
|
dir = dirs[0]
|
|
}
|
|
if dir != "" {
|
|
acct.dirlist.Select(dir)
|
|
}
|
|
acct.msglist.SetInitDone()
|
|
acct.newConn = true
|
|
}
|
|
case *types.Directory:
|
|
store, ok := acct.dirlist.MsgStore(msg.Dir.Name)
|
|
if !ok {
|
|
store = acct.newStore(msg.Dir.Name)
|
|
}
|
|
acct.dirlist.SetMsgStore(msg.Dir, store)
|
|
case *types.DirectoryInfo:
|
|
acct.dirlist.Update(msg)
|
|
case *types.DirectoryContents:
|
|
if store, ok := acct.dirlist.MsgStore(msg.Directory); ok {
|
|
store.Update(msg)
|
|
if store == acct.Store() {
|
|
if acct.msglist.Store() == nil {
|
|
acct.msglist.SetStore(store)
|
|
}
|
|
acct.SetStatus(state.Threading(store.ThreadedView()))
|
|
}
|
|
}
|
|
if acct.newConn && len(msg.Uids) == 0 {
|
|
acct.checkMailOnStartup()
|
|
}
|
|
case *types.DirectoryThreaded:
|
|
if store, ok := acct.dirlist.MsgStore(msg.Directory); ok {
|
|
store.Update(msg)
|
|
if store == acct.Store() {
|
|
if acct.msglist.Store() == nil {
|
|
acct.msglist.SetStore(store)
|
|
}
|
|
acct.SetStatus(state.Threading(store.ThreadedView()))
|
|
}
|
|
}
|
|
if acct.newConn && len(msg.Threads) == 0 {
|
|
acct.checkMailOnStartup()
|
|
}
|
|
case *types.FullMessage:
|
|
if store, ok := acct.dirlist.SelectedMsgStore(); ok {
|
|
store.Update(msg)
|
|
}
|
|
case *types.MessageInfo:
|
|
if store, ok := acct.dirlist.MsgStore(msg.Info.Directory); ok {
|
|
if msg.InResponseTo() == nil || msg.ReplaceFlags {
|
|
// This is a server generated message update, e.g. a
|
|
// notification that a message has changed (this will happen
|
|
// mostly for flags according to section 2.3.1.1 alinea 4 in
|
|
// the IMAP4rev1 RFC), or a forced flag update.
|
|
// In case the store and the notification disagree on the Seen
|
|
// flag, trust the notification.
|
|
seen_in_sync := true
|
|
if msg_in_store, ok := store.Messages[msg.Info.Uid]; ok && msg_in_store != nil {
|
|
seen_in_sync = msg_in_store.Flags.Has(models.SeenFlag) ==
|
|
msg.Info.Flags.Has(models.SeenFlag)
|
|
}
|
|
if !seen_in_sync {
|
|
if dir := acct.dirlist.Directory(msg.Info.Directory); dir != nil {
|
|
// Our view of Unseen is out-of-sync with the server's;
|
|
// update it now.
|
|
if msg.Info.Flags.Has(models.SeenFlag) {
|
|
dir.Unseen -= 1
|
|
} else {
|
|
dir.Unseen += 1
|
|
}
|
|
dir.Unseen = acct.ensurePositive(dir.Unseen, "Unseen")
|
|
}
|
|
}
|
|
}
|
|
store.Update(msg)
|
|
}
|
|
case *types.MessagesDeleted:
|
|
if dir := acct.dirlist.Directory(msg.Directory); dir != nil {
|
|
acct.updateDirCounts(dir.Name, msg.Uids, true)
|
|
}
|
|
if store, ok := acct.dirlist.MsgStore(msg.Directory); ok {
|
|
store.Update(msg)
|
|
}
|
|
case *types.MessagesCopied:
|
|
acct.updateDirCounts(msg.Destination, msg.Uids, false)
|
|
case *types.MessagesMoved:
|
|
acct.updateDirCounts(msg.Destination, msg.Uids, false)
|
|
case *types.LabelList:
|
|
acct.labels = msg.Labels
|
|
case *types.ConnError:
|
|
log.Errorf("[%s] connection error: %v", acct.acct.Name, msg.Error)
|
|
acct.SetStatus(state.SetConnected(false))
|
|
acct.PushError(msg.Error)
|
|
acct.msglist.SetStore(nil)
|
|
// Exponential backoff: 1.8^retries seconds, capped at ReconnectMaxWait
|
|
var wait time.Duration
|
|
if acct.reconnectRetries > 0 {
|
|
backoff := math.Pow(1.8, float64(acct.reconnectRetries))
|
|
wait = time.Duration(backoff) * time.Second
|
|
maxWait := acct.acct.ReconnectMaxWait
|
|
if wait > maxWait {
|
|
wait = maxWait
|
|
}
|
|
}
|
|
acct.reconnectRetries++
|
|
time.AfterFunc(wait, func() {
|
|
acct.worker.PostAction(context.TODO(), &types.Reconnect{}, nil)
|
|
})
|
|
case *types.Error:
|
|
log.Errorf("[%s] unexpected error: %v", acct.acct.Name, msg.Error)
|
|
acct.PushError(msg.Error)
|
|
}
|
|
acct.UpdateStatus()
|
|
acct.setTitle()
|
|
}
|
|
|
|
func (acct *AccountView) ensurePositive(val int, name string) int {
|
|
if val < 0 {
|
|
acct.worker.Errorf("Unexpected negative value (%d) for %s", val, name)
|
|
return 0
|
|
}
|
|
return val
|
|
}
|
|
|
|
func (acct *AccountView) updateDirCounts(destination string, uids []models.UID, deleted bool) {
|
|
// Only update the destination destDir if it is initialized
|
|
if destDir := acct.dirlist.Directory(destination); destDir != nil {
|
|
var recent, unseen int
|
|
var accurate bool = true
|
|
store := acct.Store()
|
|
if store == nil {
|
|
// This could happen for example if a disconnection happened,
|
|
// and we can't do anything but bail out.
|
|
accurate = false
|
|
} else {
|
|
for _, uid := range uids {
|
|
// Get the message from the originating store
|
|
msg, ok := store.Messages[uid]
|
|
if !ok {
|
|
continue
|
|
}
|
|
// If message that was not yet loaded is copied
|
|
if msg == nil {
|
|
accurate = false
|
|
break
|
|
}
|
|
if msg.Flags.Has(models.RecentFlag) {
|
|
recent++
|
|
}
|
|
seen := msg.Flags.Has(models.SeenFlag)
|
|
if !seen {
|
|
// If the message is unseen, the directory's current unseen
|
|
// count is off by one and (1) too low if the message is new,
|
|
// or (2) too high if the message has been deleted.
|
|
if !deleted {
|
|
unseen++
|
|
} else {
|
|
unseen--
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if accurate {
|
|
destDir.Recent += recent
|
|
destDir.Unseen += unseen
|
|
}
|
|
if !deleted {
|
|
destDir.Exists += len(uids)
|
|
} else {
|
|
destDir.Exists -= len(uids)
|
|
}
|
|
destDir.Unseen = acct.ensurePositive(destDir.Unseen, "Unseen")
|
|
destDir.Recent = acct.ensurePositive(destDir.Recent, "Recent")
|
|
destDir.Exists = acct.ensurePositive(destDir.Exists, "Exists")
|
|
} else {
|
|
acct.worker.Errorf("Skipping unknown directory %s", destination)
|
|
}
|
|
}
|
|
|
|
func (acct *AccountView) SortCriteria(uiConf *config.UIConfig) []*types.SortCriterion {
|
|
if uiConf == nil {
|
|
return nil
|
|
}
|
|
if len(uiConf.Sort) == 0 {
|
|
return nil
|
|
}
|
|
criteria, err := sort.GetSortCriteria(uiConf.Sort)
|
|
if err != nil {
|
|
acct.PushError(fmt.Errorf("ui sort: %w", err))
|
|
return nil
|
|
}
|
|
return criteria
|
|
}
|
|
|
|
func (acct *AccountView) GetSortCriteria() []*types.SortCriterion {
|
|
return acct.SortCriteria(acct.UiConfig())
|
|
}
|
|
|
|
func (acct *AccountView) CheckMail() {
|
|
acct.Lock()
|
|
defer acct.Unlock()
|
|
if acct.checkingMail {
|
|
return
|
|
}
|
|
// Exclude selected mailbox, per IMAP specification
|
|
exclude := append(acct.AccountConfig().CheckMailExclude, acct.dirlist.Selected()) //nolint:gocritic // intentional append to different slice
|
|
dirs := acct.dirlist.List()
|
|
dirs = acct.dirlist.FilterDirs(dirs, acct.AccountConfig().CheckMailInclude, false)
|
|
dirs = acct.dirlist.FilterDirs(dirs, exclude, true)
|
|
log.Debugf("Checking for new mail on account %s", acct.Name())
|
|
acct.SetStatus(state.ConnectionActivity("Checking for new mail..."))
|
|
msg := &types.CheckMail{
|
|
Directories: dirs,
|
|
Command: acct.acct.CheckMailCmd,
|
|
Timeout: acct.acct.CheckMailTimeout,
|
|
}
|
|
acct.checkingMail = true
|
|
|
|
var cb func(types.WorkerMessage)
|
|
cb = func(response types.WorkerMessage) {
|
|
dirsMsg, ok := response.(*types.CheckMailDirectories)
|
|
if ok {
|
|
checkMailMsg := &types.CheckMail{
|
|
Directories: dirsMsg.Directories,
|
|
Command: acct.acct.CheckMailCmd,
|
|
Timeout: acct.acct.CheckMailTimeout,
|
|
}
|
|
acct.worker.PostAction(context.TODO(), checkMailMsg, cb)
|
|
} else { // Done
|
|
acct.SetStatus(state.ConnectionActivity(""))
|
|
acct.Lock()
|
|
acct.checkingMail = false
|
|
acct.Unlock()
|
|
}
|
|
}
|
|
acct.worker.PostAction(context.TODO(), msg, cb)
|
|
}
|
|
|
|
// CheckMailReset resets the check-mail timer
|
|
func (acct *AccountView) CheckMailReset() {
|
|
if acct.ticker != nil {
|
|
d := acct.AccountConfig().CheckMail
|
|
acct.ticker = time.NewTicker(d)
|
|
}
|
|
}
|
|
|
|
func (acct *AccountView) checkMailOnStartup() {
|
|
if acct.AccountConfig().CheckMail.Minutes() > 0 {
|
|
acct.newConn = false
|
|
acct.CheckMail()
|
|
}
|
|
}
|
|
|
|
func (acct *AccountView) CheckMailTimer(d time.Duration) {
|
|
acct.ticker = time.NewTicker(d)
|
|
go func() {
|
|
defer log.PanicHandler()
|
|
for range acct.ticker.C {
|
|
if !acct.state.Connected {
|
|
continue
|
|
}
|
|
acct.CheckMail()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (acct *AccountView) closeSplit() {
|
|
if acct.split != nil {
|
|
acct.split.Close()
|
|
}
|
|
acct.splitSize = 0
|
|
acct.splitDir = config.SPLIT_NONE
|
|
acct.split = nil
|
|
acct.buildGrid()
|
|
ui.Invalidate()
|
|
}
|
|
|
|
func (acct *AccountView) updateSplitView(msg *models.MessageInfo) {
|
|
uiConf := acct.UiConfig()
|
|
if !acct.splitLoaded {
|
|
switch uiConf.MessageListSplit.Direction {
|
|
case config.SPLIT_HORIZONTAL:
|
|
acct.Split(uiConf.MessageListSplit.Size)
|
|
case config.SPLIT_VERTICAL:
|
|
acct.Vsplit(uiConf.MessageListSplit.Size)
|
|
}
|
|
acct.splitLoaded = true
|
|
}
|
|
if acct.splitSize == 0 || !acct.splitLoaded {
|
|
return
|
|
}
|
|
if acct.splitDebounce != nil {
|
|
acct.splitDebounce.Stop()
|
|
}
|
|
fn := func() {
|
|
if acct.split != nil {
|
|
acct.grid.RemoveChild(acct.split)
|
|
acct.split.Close()
|
|
}
|
|
lib.NewMessageStoreView(msg, false, acct.Store(), CryptoProvider(), DecryptKeys,
|
|
func(view lib.MessageView, err error) {
|
|
if err != nil {
|
|
PushError(err.Error())
|
|
return
|
|
}
|
|
viewer, err := NewMessageViewer(acct, view)
|
|
if err != nil {
|
|
PushError(err.Error())
|
|
return
|
|
}
|
|
acct.split = viewer
|
|
switch acct.splitDir {
|
|
case config.SPLIT_HORIZONTAL:
|
|
if acct.hasSidebar() {
|
|
acct.grid.AddChild(acct.split).At(1, 1)
|
|
} else {
|
|
acct.grid.AddChild(acct.split).At(1, 0)
|
|
}
|
|
case config.SPLIT_VERTICAL:
|
|
if acct.hasSidebar() {
|
|
acct.grid.AddChild(acct.split).At(0, 2)
|
|
} else {
|
|
acct.grid.AddChild(acct.split).At(0, 1)
|
|
}
|
|
}
|
|
// If the user wants to, start a timer to mark the message read
|
|
// if it stays in the message viewer longer than the requested
|
|
// delay.
|
|
if !uiConf.AutoMarkReadInSplit {
|
|
return
|
|
}
|
|
acct.splitDebounce = time.AfterFunc(uiConf.AutoMarkReadInSplitDelay, func() {
|
|
if view == nil || view.MessageInfo() == nil || view.Store() == nil {
|
|
return
|
|
}
|
|
view.Store().Flag([]models.UID{view.MessageInfo().Uid}, models.SeenFlag, true, nil)
|
|
})
|
|
})
|
|
}
|
|
acct.splitDebounce = time.AfterFunc(100*time.Millisecond, func() {
|
|
ui.QueueFunc(fn)
|
|
})
|
|
}
|
|
|
|
func (acct *AccountView) SplitSize() int {
|
|
return acct.splitSize
|
|
}
|
|
|
|
func (acct *AccountView) SetSplitSize(n int) {
|
|
if n == 0 {
|
|
acct.closeSplit()
|
|
}
|
|
acct.splitSize = n
|
|
}
|
|
|
|
func (acct *AccountView) ToggleHeaders() {
|
|
config.Viewer().ShowHeaders = !config.Viewer().ShowHeaders
|
|
if acct.splitSize == 0 {
|
|
return
|
|
}
|
|
msg, err := acct.SelectedMessage()
|
|
if err != nil {
|
|
log.Debugf("split: load message error: %v", err)
|
|
}
|
|
acct.updateSplitView(msg)
|
|
}
|
|
|
|
// ToggleSidebar toggles the visibility of the sidebar
|
|
func (acct *AccountView) ToggleSidebar() {
|
|
acct.sidebarHidden = !acct.sidebarHidden
|
|
switch acct.splitDir {
|
|
case config.SPLIT_HORIZONTAL:
|
|
acct.splitDir = config.SPLIT_NONE
|
|
acct.Split(acct.splitSize)
|
|
case config.SPLIT_VERTICAL:
|
|
acct.splitDir = config.SPLIT_NONE
|
|
acct.Vsplit(acct.splitSize)
|
|
default:
|
|
acct.buildGrid()
|
|
}
|
|
ui.Invalidate()
|
|
}
|
|
|
|
func (acct *AccountView) hasSidebar() bool {
|
|
return !acct.sidebarHidden && acct.UiConfig().SidebarWidth > 0
|
|
}
|
|
|
|
// buildGrid creates the basic grid layout without splits
|
|
func (acct *AccountView) buildGrid() {
|
|
sidebar := acct.UiConfig().SidebarWidth
|
|
if !acct.hasSidebar() {
|
|
acct.grid = ui.NewGrid().Rows([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
}).Columns([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
})
|
|
acct.grid.AddChild(acct.msglist).At(0, 0)
|
|
} else {
|
|
acct.grid = ui.NewGrid().Rows([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
}).Columns([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_EXACT, Size: func() int {
|
|
return sidebar
|
|
}},
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
})
|
|
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig()))
|
|
acct.grid.AddChild(acct.msglist).At(0, 1)
|
|
}
|
|
}
|
|
|
|
// Split splits the message list view horizontally. The message list will be n
|
|
// rows high. If n is 0, any existing split is removed
|
|
func (acct *AccountView) Split(n int) {
|
|
acct.SetSplitSize(n)
|
|
if acct.splitDir == config.SPLIT_HORIZONTAL || n == 0 {
|
|
return
|
|
}
|
|
acct.splitDir = config.SPLIT_HORIZONTAL
|
|
|
|
rowSpecs := []ui.GridSpec{
|
|
// Add 1 so that the splitSize is the number of visible messages
|
|
{Strategy: ui.SIZE_EXACT, Size: func() int { return acct.SplitSize() + 1 }},
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
}
|
|
|
|
msglistCol := 1
|
|
if !acct.hasSidebar() {
|
|
acct.grid = ui.NewGrid().Rows(rowSpecs).Columns([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
})
|
|
msglistCol = 0
|
|
} else {
|
|
acct.grid = ui.NewGrid().Rows(rowSpecs).Columns([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_EXACT, Size: func() int {
|
|
return acct.UiConfig().SidebarWidth
|
|
}},
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
})
|
|
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig())).Span(2, 1)
|
|
}
|
|
|
|
acct.grid.AddChild(ui.NewBordered(acct.msglist, ui.BORDER_BOTTOM, acct.UiConfig())).At(0, msglistCol)
|
|
acct.split, _ = NewMessageViewer(acct, nil)
|
|
acct.grid.AddChild(acct.split).At(1, msglistCol)
|
|
msg, err := acct.SelectedMessage()
|
|
if err != nil {
|
|
log.Debugf("split: load message error: %v", err)
|
|
}
|
|
acct.updateSplitView(msg)
|
|
}
|
|
|
|
// Vsplit splits the message list view vertically. The message list will be n
|
|
// rows wide. If n is 0, any existing split is removed
|
|
func (acct *AccountView) Vsplit(n int) {
|
|
acct.SetSplitSize(n)
|
|
if acct.splitDir == config.SPLIT_VERTICAL || n == 0 {
|
|
return
|
|
}
|
|
acct.splitDir = config.SPLIT_VERTICAL
|
|
|
|
rowSpecs := []ui.GridSpec{
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
}
|
|
|
|
msglistCol := 1
|
|
splitCol := 2
|
|
if !acct.hasSidebar() {
|
|
acct.grid = ui.NewGrid().Rows(rowSpecs).Columns([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_EXACT, Size: acct.SplitSize},
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
})
|
|
msglistCol = 0
|
|
splitCol = 1
|
|
} else {
|
|
acct.grid = ui.NewGrid().Rows(rowSpecs).Columns([]ui.GridSpec{
|
|
{Strategy: ui.SIZE_EXACT, Size: func() int {
|
|
return acct.UiConfig().SidebarWidth
|
|
}},
|
|
{Strategy: ui.SIZE_EXACT, Size: acct.SplitSize},
|
|
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
|
|
})
|
|
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.UiConfig())).At(0, 0)
|
|
}
|
|
|
|
acct.grid.AddChild(ui.NewBordered(acct.msglist, ui.BORDER_RIGHT, acct.UiConfig())).At(0, msglistCol)
|
|
acct.split, _ = NewMessageViewer(acct, nil)
|
|
acct.grid.AddChild(acct.split).At(0, splitCol)
|
|
msg, err := acct.SelectedMessage()
|
|
if err != nil {
|
|
log.Debugf("split: load message error: %v", err)
|
|
}
|
|
acct.updateSplitView(msg)
|
|
}
|
|
|
|
// setTitle executes the title template and sets the tab title
|
|
func (acct *AccountView) setTitle() {
|
|
if acct.tab == nil {
|
|
return
|
|
}
|
|
|
|
data := state.NewDataSetter()
|
|
data.SetAccount(acct.acct)
|
|
data.SetFolder(acct.Directories().SelectedDirectory())
|
|
data.SetRUE(acct.dirlist.List(), acct.dirlist.GetRUECount)
|
|
data.SetState(&acct.state)
|
|
data.SetHasNew(acct.hasNew)
|
|
|
|
var buf bytes.Buffer
|
|
err := templates.Render(acct.UiConfig().TabTitleAccount, &buf, data.Data())
|
|
if err != nil {
|
|
acct.PushError(err)
|
|
return
|
|
}
|
|
acct.tab.SetTitle(buf.String())
|
|
}
|