init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/ui"
)
type Align struct {
Pos app.AlignPosition `opt:"pos" metavar:"top|center|bottom" action:"ParsePos" complete:"CompletePos" desc:"Position."`
}
func init() {
commands.Register(Align{})
}
func (Align) Description() string {
return "Align the message list view."
}
var posNames []string = []string{"top", "center", "bottom"}
func (a *Align) ParsePos(arg string) error {
switch arg {
case "top":
a.Pos = app.AlignTop
case "center":
a.Pos = app.AlignCenter
case "bottom":
a.Pos = app.AlignBottom
default:
return errors.New("invalid alignment")
}
return nil
}
func (a *Align) CompletePos(arg string) []string {
return commands.FilterList(posNames, arg, commands.QuoteSpace)
}
func (Align) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (Align) Aliases() []string {
return []string{"align"}
}
func (a Align) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("no account selected")
}
msgList := acct.Messages()
if msgList == nil {
return errors.New("no message list available")
}
msgList.AlignMessage(a.Pos)
ui.Invalidate()
return nil
}
+143
View File
@@ -0,0 +1,143 @@
package account
import (
"errors"
"strings"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~rjarry/go-opt/v2"
)
type ChangeFolder struct {
Account string `opt:"-a" complete:"CompleteAccount" desc:"Change to specified account."`
Folder string `opt:"..." complete:"CompleteFolderAndNotmuch" desc:"Folder name."`
}
func init() {
commands.Register(ChangeFolder{})
}
func (ChangeFolder) Description() string {
return "Change the folder shown in the message list."
}
func (ChangeFolder) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (ChangeFolder) Aliases() []string {
return []string{"cf"}
}
func (c *ChangeFolder) CompleteAccount(arg string) []string {
return commands.FilterList(app.AccountNames(), arg, commands.QuoteSpace)
}
func (c *ChangeFolder) CompleteFolderAndNotmuch(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
retval := commands.FilterList(
acct.Directories().List(), arg,
func(s string) string {
dir := acct.Directories().Directory(s)
if dir != nil && dir.Role != models.QueryRole {
s = opt.QuoteArg(s)
}
return s
},
)
if acct.AccountConfig().Backend == "notmuch" {
notmuchcomps := handleNotmuchComplete(arg)
for _, prefix := range notmuch_search_terms {
if strings.HasPrefix(arg, prefix) {
return notmuchcomps
}
}
retval = append(retval, notmuchcomps...)
}
return retval
}
func (c ChangeFolder) Execute([]string) error {
var target string
var acct *app.AccountView
var err error
args := opt.LexArgs(c.Folder)
if c.Account != "" {
acct, err = app.Account(c.Account)
if err != nil {
return err
}
} else {
acct = app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
}
if args.Count() == 0 {
return errors.New("<folder> is required. Usage: cf [-a <account>] <folder>")
}
if acct.AccountConfig().Backend == "notmuch" {
// With notmuch, :cf can change to a "dynamic folder" that
// contains the result of a query. Preserve the entered
// arguments verbatim.
target = args.String()
} else {
if args.Count() != 1 {
return errors.New("Unexpected argument(s). Usage: cf [-a <account>] <folder>")
}
target = args.Arg(0)
}
finalize := func(msg types.WorkerMessage) {
handleDirOpenResponse(acct, msg)
}
dirlist := acct.Directories()
if dirlist == nil {
return errors.New("No directory list found")
}
if target == "-" {
dir := dirlist.Previous()
if dir != "" {
target = dir
} else {
return errors.New("No previous folder to return to")
}
}
dirlist.Open(target, "", 0*time.Second, finalize, false)
return nil
}
func handleDirOpenResponse(acct *app.AccountView, msg types.WorkerMessage) {
// As we're waiting for the worker to report status we must run
// the rest of the actions in this callback.
switch msg := msg.(type) {
case *types.Error:
app.PushError(msg.Error.Error())
case *types.Done:
// reset store filtering if we switched folders
store := acct.Store()
if store != nil {
store.ApplyClear()
acct.SetStatus(state.SearchFilterClear())
}
// focus account tab
acct.Select()
}
}
+36
View File
@@ -0,0 +1,36 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type CheckMail struct{}
func init() {
commands.Register(CheckMail{})
}
func (CheckMail) Description() string {
return "Check for new mail on the selected account."
}
func (CheckMail) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (CheckMail) Aliases() []string {
return []string{"check-mail"}
}
func (CheckMail) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
acct.CheckMailReset()
acct.CheckMail()
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/state"
)
type Clear struct {
Selected bool `opt:"-s" desc:"Select first message after clearing."`
}
func init() {
commands.Register(Clear{})
}
func (Clear) Description() string {
return "Clear the current search or filter criteria."
}
func (Clear) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (Clear) Aliases() []string {
return []string{"clear"}
}
func (c Clear) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := acct.Store()
if store == nil {
return errors.New("Cannot perform action. Messages still loading")
}
if c.Selected {
defer store.Select("")
}
store.ApplyClear()
acct.SetStatus(state.SearchFilterClear())
return nil
}
+95
View File
@@ -0,0 +1,95 @@
package account
import (
"errors"
"fmt"
"io"
gomail "net/mail"
"regexp"
"strings"
"github.com/emersion/go-message/mail"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
)
type Compose struct {
Headers string `opt:"-H" action:"ParseHeader" desc:"Add the specified header to the message."`
Template string `opt:"-T" complete:"CompleteTemplate" desc:"Template name."`
Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."`
NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."`
SkipEditor bool `opt:"-s" desc:"Skip the editor and go directly to the review screen."`
Body string `opt:"..." required:"false"`
}
func init() {
commands.Register(Compose{})
}
func (Compose) Description() string {
return "Open the compose window to write a new email."
}
func (Compose) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (c *Compose) ParseHeader(arg string) error {
if strings.Contains(arg, ":") {
// ensure first colon is followed by a single space
re := regexp.MustCompile(`^(.*?):\s*(.*)`)
c.Headers += re.ReplaceAllString(arg, "$1: $2\r\n")
} else {
c.Headers += arg + ":\r\n"
}
return nil
}
func (*Compose) CompleteTemplate(arg string) []string {
return commands.GetTemplates(arg)
}
func (Compose) Aliases() []string {
return []string{"compose"}
}
func (c Compose) Execute(args []string) error {
if c.Headers != "" {
if c.Body != "" {
c.Body = c.Headers + "\r\n" + c.Body
} else {
c.Body = c.Headers + "\r\n\r\n"
}
}
if c.Template == "" {
c.Template = config.Templates.NewMessage
}
editHeaders := (config.Compose.EditHeaders || c.Edit) && !c.NoEdit
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
msg, err := gomail.ReadMessage(strings.NewReader(c.Body))
if errors.Is(err, io.EOF) { // completely empty
msg = &gomail.Message{Body: strings.NewReader("")}
} else if err != nil {
return fmt.Errorf("mail.ReadMessage: %w", err)
}
headers := mail.HeaderFromMap(msg.Header)
composer, err := app.NewComposer(acct,
acct.AccountConfig(), acct.Worker(), editHeaders,
c.Template, &headers, nil, msg.Body)
if err != nil {
return err
}
composer.Tab = app.NewTab(composer, "New email")
if c.SkipEditor {
composer.Terminal().Close()
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type Connection struct{}
func init() {
commands.Register(Connection{})
}
func (Connection) Description() string {
return "Disconnect or reconnect the current account."
}
func (Connection) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (Connection) Aliases() []string {
return []string{"connect", "disconnect"}
}
func (c Connection) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
cb := func(msg types.WorkerMessage) {
acct.SetStatus(state.ConnectionActivity(""))
}
if args[0] == "connect" {
acct.Worker().PostAction(&types.Connect{}, cb)
acct.SetStatus(state.ConnectionActivity("Connecting..."))
} else {
acct.Worker().PostAction(&types.Disconnect{}, cb)
acct.SetStatus(state.ConnectionActivity("Disconnecting..."))
}
return nil
}
+52
View File
@@ -0,0 +1,52 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type ExpandCollapseFolder struct {
Folder string `opt:"folder" required:"false" complete:"CompleteFolder" desc:"Folder name."`
}
func init() {
commands.Register(ExpandCollapseFolder{})
}
func (ExpandCollapseFolder) Description() string {
return "Expand or collapse the current folder."
}
func (ExpandCollapseFolder) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (ExpandCollapseFolder) Aliases() []string {
return []string{"expand-folder", "collapse-folder"}
}
func (*ExpandCollapseFolder) CompleteFolder(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
return commands.FilterList(acct.Directories().List(), arg, nil)
}
func (e ExpandCollapseFolder) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
if e.Folder == "" {
e.Folder = acct.Directories().Selected()
}
if args[0] == "expand-folder" {
acct.Directories().ExpandFolder(e.Folder)
} else {
acct.Directories().CollapseFolder(e.Folder)
}
return nil
}
+198
View File
@@ -0,0 +1,198 @@
package account
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/xdg"
"git.sr.ht/~rjarry/aerc/models"
mboxer "git.sr.ht/~rjarry/aerc/worker/mbox"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type ExportMbox struct {
Filename string `opt:"filename" complete:"CompleteFilename" desc:"Output file path."`
}
func init() {
commands.Register(ExportMbox{})
}
func (ExportMbox) Description() string {
return "Export messages in the current folder to an mbox file."
}
func (ExportMbox) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (ExportMbox) Aliases() []string {
return []string{"export-mbox"}
}
func (*ExportMbox) CompleteFilename(arg string) []string {
return commands.CompletePath(arg, false)
}
func (e ExportMbox) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := acct.Store()
if store == nil {
return errors.New("No message store selected")
}
e.Filename = xdg.ExpandHome(e.Filename)
fi, err := os.Stat(e.Filename)
if err == nil && fi.IsDir() {
if path := acct.SelectedDirectory(); path != "" {
if f := filepath.Base(path); f != "" {
e.Filename = filepath.Join(e.Filename, f+".mbox")
}
}
}
app.PushStatus("Exporting to "+e.Filename, 10*time.Second)
// uids of messages to export
var uids []models.UID
// check if something is marked - we export that then
msgProvider, ok := app.SelectedTabContent().(app.ProvidesMessages)
if !ok {
msgProvider = app.SelectedAccount()
}
if msgProvider != nil {
marked, err := msgProvider.MarkedMessages()
if err == nil && len(marked) > 0 {
uids, err = sortMarkedUids(marked, store)
if err != nil {
return err
}
}
}
// if no messages were marked, we export everything
if len(uids) == 0 {
var err error
uids, err = sortAllUids(store)
if err != nil {
return err
}
}
go func() {
defer log.PanicHandler()
file, err := os.Create(e.Filename)
if err != nil {
log.Errorf("failed to create file: %v", err)
app.PushError(err.Error())
return
}
defer file.Close()
var mu sync.Mutex
var ctr uint
var retries int
done := make(chan bool)
t := time.Now()
total := len(uids)
for len(uids) > 0 {
if retries > 0 {
if retries > 10 {
errorMsg := fmt.Sprintf("too many retries: %d; stopping export", retries)
log.Errorf(errorMsg)
app.PushError(args[0] + " " + errorMsg)
break
}
sleeping := time.Duration(retries * 1e9 * 2)
log.Debugf("sleeping for %s before retrying; retries: %d", sleeping, retries)
time.Sleep(sleeping)
}
log.Debugf("fetching %d for export", len(uids))
acct.Worker().PostAction(&types.FetchFullMessages{
Uids: uids,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
done <- true
case *types.Error:
log.Errorf("failed to fetch message: %v", msg.Error)
app.PushError(args[0] + " error encountered: " + msg.Error.Error())
done <- false
case *types.FullMessage:
mu.Lock()
err := mboxer.Write(file, msg.Content.Reader, "", t)
if err != nil {
log.Warnf("failed to write mbox: %v", err)
}
for i, uid := range uids {
if uid == msg.Content.Uid {
uids = append(uids[:i], uids[i+1:]...)
break
}
}
ctr++
mu.Unlock()
}
})
if ok := <-done; ok {
break
}
retries++
}
statusInfo := fmt.Sprintf("Exported %d of %d messages to %s.", ctr, total, e.Filename)
app.PushStatus(statusInfo, 10*time.Second)
log.Debugf(statusInfo)
}()
return nil
}
func sortMarkedUids(marked []models.UID, store *lib.MessageStore) ([]models.UID, error) {
lookup := map[models.UID]bool{}
for _, uid := range marked {
lookup[uid] = true
}
uids := []models.UID{}
iter := store.UidsIterator()
for iter.Next() {
uid, ok := iter.Value().(models.UID)
if !ok {
return nil, errors.New("Invalid message UID value")
}
_, marked := lookup[uid]
if marked {
uids = append(uids, uid)
}
}
return uids, nil
}
func sortAllUids(store *lib.MessageStore) ([]models.UID, error) {
uids := []models.UID{}
iter := store.UidsIterator()
for iter.Next() {
uid, ok := iter.Value().(models.UID)
if !ok {
return nil, errors.New("Invalid message UID value")
}
uids = append(uids, uid)
}
return uids, nil
}
+189
View File
@@ -0,0 +1,189 @@
package account
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"sync/atomic"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/xdg"
"git.sr.ht/~rjarry/aerc/models"
mboxer "git.sr.ht/~rjarry/aerc/worker/mbox"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type ImportMbox struct {
Path string `opt:"path" complete:"CompleteFilename" desc:"Input file path or URL."`
}
func init() {
commands.Register(ImportMbox{})
}
func (ImportMbox) Description() string {
return "Import all messages from an (gzipped) mbox file to the current folder."
}
func (ImportMbox) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (ImportMbox) Aliases() []string {
return []string{"import-mbox"}
}
func (*ImportMbox) CompleteFilename(arg string) []string {
return commands.CompletePath(arg, false)
}
func (i ImportMbox) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := acct.Store()
if store == nil {
return errors.New("No message store selected")
}
folder := acct.SelectedDirectory()
if folder == "" {
return errors.New("No directory selected")
}
importFolder := func(r io.ReadCloser) {
defer log.PanicHandler()
defer r.Close()
messages, err := mboxer.Read(r)
if err != nil {
app.PushError(err.Error())
return
}
var appended uint32
for i, m := range messages {
done := make(chan bool)
var retries int = 4
for retries > 0 {
var buf bytes.Buffer
r, err := m.NewReader()
if err != nil {
log.Errorf("could not get reader for uid %d", m.UID())
break
}
nbytes, _ := io.Copy(&buf, r)
store.Append(
folder,
models.SeenFlag,
time.Now(),
&buf,
int(nbytes),
func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Unsupported:
errMsg := fmt.Sprintf("%s: AppendMessage is unsupported", args[0])
log.Errorf(errMsg)
app.PushError(errMsg)
return
case *types.Error:
log.Errorf("AppendMessage failed: %v", msg.Error)
done <- false
case *types.Done:
atomic.AddUint32(&appended, 1)
done <- true
}
},
)
select {
case ok := <-done:
if ok {
retries = 0
} else {
// error encountered; try to append again after a quick nap
retries -= 1
sleeping := time.Duration((5 - retries) * 1e9)
log.Debugf("sleeping for %s before append message %d again", sleeping, i)
time.Sleep(sleeping)
}
case <-time.After(30 * time.Second):
log.Warnf("timed-out; appended %d of %d", appended, len(messages))
return
}
}
}
infoStr := fmt.Sprintf("%s: imported %d of %d successfully.", args[0], appended, len(messages))
log.Debugf(infoStr)
app.PushSuccess(infoStr)
}
var buf []byte
path := i.Path
if ok, err := regexp.MatchString("^(http[s]\\:|www\\.)", path); ok && err == nil {
resp, err := http.Get(path)
if err != nil {
return err
}
buf, err = io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
return err
}
} else {
path = xdg.ExpandHome(path)
buf, err = os.ReadFile(path)
if err != nil {
return err
}
}
var r io.ReadCloser
// detect gzip format compressed files as specified in RFC 1952
if len(buf) >= 2 && buf[0] == 0x1f && buf[1] == 0x8b {
var err error
r, err = gzip.NewReader(bytes.NewReader(buf))
if err != nil {
return err
}
} else {
r = io.NopCloser(bytes.NewReader(buf))
}
statusInfo := fmt.Sprintln("Importing", path, "to folder", folder)
app.PushStatus(statusInfo, 10*time.Second)
log.Debugf(statusInfo)
if len(store.Uids()) > 0 {
confirm := app.NewSelectorDialog(
"Selected directory is not empty",
fmt.Sprintf("Import mbox file to %s anyways?", folder),
[]string{"No", "Yes"}, 0, app.SelectedAccountUiConfig(),
func(option string, err error) {
app.CloseDialog()
if option == "Yes" {
go importFolder(r)
} else {
_ = r.Close()
}
},
)
app.AddDialog(confirm)
} else {
go importFolder(r)
}
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package account
import (
"errors"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~rjarry/go-opt/v2"
)
type MakeDir struct {
Folder string `opt:"folder" complete:"CompleteFolder" desc:"Folder name."`
}
func init() {
commands.Register(MakeDir{})
}
func (MakeDir) Description() string {
return "Create and change to a new folder."
}
func (MakeDir) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (MakeDir) Aliases() []string {
return []string{"mkdir"}
}
func (*MakeDir) CompleteFolder(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
sep := app.SelectedAccount().Worker().PathSeparator()
return commands.FilterList(
acct.Directories().List(), arg,
func(s string) string {
return opt.QuoteArg(s) + sep
},
)
}
func (m MakeDir) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
acct.Worker().PostAction(&types.CreateDirectory{
Directory: m.Folder,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
app.PushStatus("Directory created.", 10*time.Second)
acct.Directories().Open(m.Folder, "", 0, nil, false)
case *types.Error:
app.PushError(msg.Error.Error())
}
})
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type NextPrevFolder struct {
Offset int `opt:"n" default:"1"`
}
func init() {
commands.Register(NextPrevFolder{})
}
func (NextPrevFolder) Description() string {
return "Cycle to the next or previous folder shown in the sidebar."
}
func (NextPrevFolder) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (NextPrevFolder) Aliases() []string {
return []string{"next-folder", "prev-folder"}
}
func (np NextPrevFolder) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
if args[0] == "prev-folder" {
acct.Directories().NextPrev(-np.Offset)
} else {
acct.Directories().NextPrev(np.Offset)
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/ui"
)
type NextPrevResult struct{}
func init() {
commands.Register(NextPrevResult{})
}
func (NextPrevResult) Description() string {
return "Select the next or previous search result."
}
func (NextPrevResult) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (NextPrevResult) Aliases() []string {
return []string{"next-result", "prev-result"}
}
func (NextPrevResult) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
if args[0] == "prev-result" {
store := acct.Store()
if store != nil {
store.PrevResult()
}
ui.Invalidate()
} else {
store := acct.Store()
if store != nil {
store.NextResult()
}
ui.Invalidate()
}
return nil
}
+108
View File
@@ -0,0 +1,108 @@
package account
import (
"errors"
"fmt"
"strconv"
"strings"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type NextPrevMsg struct {
Amount int `opt:"n" default:"1" metavar:"<n>[%]" action:"ParseAmount"`
Percent bool
}
func init() {
commands.Register(NextPrevMsg{})
}
func (NextPrevMsg) Description() string {
return "Select the next or previous message in the message list."
}
func (NextPrevMsg) Context() commands.CommandContext {
return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
}
func (np *NextPrevMsg) ParseAmount(arg string) error {
if strings.HasSuffix(arg, "%") {
np.Percent = true
arg = strings.TrimSuffix(arg, "%")
}
i, err := strconv.ParseInt(arg, 10, 64)
if err != nil {
return err
}
np.Amount = int(i)
return nil
}
func (NextPrevMsg) Aliases() []string {
return []string{"next", "next-message", "prev", "prev-message"}
}
func (np NextPrevMsg) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := acct.Store()
if store == nil {
return fmt.Errorf("No message store set.")
}
n := np.Amount
if np.Percent {
n = int(float64(acct.Messages().Height()) * (float64(n) / 100.0))
}
if args[0] == "prev-message" || args[0] == "prev" {
store.NextPrev(-n)
} else {
store.NextPrev(n)
}
if mv, ok := app.SelectedTabContent().(*app.MessageViewer); ok {
reloadViewer := func(nextMsg *models.MessageInfo) {
if nextMsg.Error != nil {
app.PushError(nextMsg.Error.Error())
return
}
lib.NewMessageStoreView(nextMsg, mv.MessageView().SeenFlagSet(),
store, app.CryptoProvider(), app.DecryptKeys,
func(view lib.MessageView, err error) {
if err != nil {
app.PushError(err.Error())
return
}
nextMv, err := app.NewMessageViewer(acct, view)
if err != nil {
app.PushError(err.Error())
return
}
app.ReplaceTab(mv, nextMv,
nextMsg.Envelope.Subject, true)
})
}
if nextMsg := store.Selected(); nextMsg != nil {
reloadViewer(nextMsg)
} else {
store.FetchHeaders([]models.UID{store.SelectedUid()},
func(msg types.WorkerMessage) {
if m, ok := msg.(*types.MessageInfo); ok {
reloadViewer(m.Info)
}
})
}
}
ui.Invalidate()
return nil
}
+127
View File
@@ -0,0 +1,127 @@
package account
import (
"errors"
"strings"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type Query struct {
Account string `opt:"-a" complete:"CompleteAccount" desc:"Account name."`
Name string `opt:"-n" desc:"Force name of virtual folder."`
Force bool `opt:"-f" desc:"Replace existing query if any."`
Query string `opt:"..." complete:"CompleteNotmuch" desc:"Notmuch query."`
}
func init() {
commands.Register(Query{})
}
func (Query) Description() string {
return "Create a virtual folder using the specified notmuch query."
}
func (Query) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (Query) Aliases() []string {
return []string{"query"}
}
func (Query) CompleteAccount(arg string) []string {
return commands.FilterList(app.AccountNames(), arg, commands.QuoteSpace)
}
func (q Query) Execute([]string) error {
var acct *app.AccountView
if q.Account == "" {
acct = app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
} else {
var err error
acct, err = app.Account(q.Account)
if err != nil {
return err
}
}
if acct.AccountConfig().Backend != "notmuch" {
return errors.New(":query is only available for notmuch accounts")
}
finalize := func(msg types.WorkerMessage) {
handleDirOpenResponse(acct, msg)
}
name := q.Name
if name == "" {
name = q.Query
}
acct.Directories().Open(name, q.Query, 0*time.Second, finalize, q.Force)
return nil
}
func (*Query) CompleteNotmuch(arg string) []string {
return handleNotmuchComplete(arg)
}
var notmuch_search_terms = []string{
"from:",
"to:",
"tag:",
"date:",
"attachment:",
"mimetype:",
"subject:",
"body:",
"id:",
"thread:",
"folder:",
"path:",
}
func handleNotmuchComplete(arg string) []string {
prefixes := []string{"from:", "to:"}
for _, prefix := range prefixes {
if strings.HasPrefix(arg, prefix) {
arg = strings.TrimPrefix(arg, prefix)
return commands.FilterList(
commands.GetAddress(arg), arg,
func(v string) string { return prefix + v },
)
}
}
prefixes = []string{"tag:"}
for _, prefix := range prefixes {
if strings.HasPrefix(arg, prefix) {
arg = strings.TrimPrefix(arg, prefix)
return commands.FilterList(
commands.GetLabels(arg), arg,
func(v string) string { return prefix + v },
)
}
}
prefixes = []string{"path:", "folder:"}
dbPath := strings.TrimPrefix(app.SelectedAccount().AccountConfig().Source, "notmuch://")
for _, prefix := range prefixes {
if strings.HasPrefix(arg, prefix) {
arg = strings.TrimPrefix(arg, prefix)
return commands.FilterList(
commands.CompletePath(dbPath+arg, true), arg,
func(v string) string { return prefix + strings.TrimPrefix(v, dbPath) },
)
}
}
return commands.FilterList(notmuch_search_terms, arg, nil)
}
+88
View File
@@ -0,0 +1,88 @@
package account
import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
)
type Recover struct {
Force bool `opt:"-f" desc:"Delete recovered file after opening the composer."`
Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."`
NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."`
File string `opt:"file" complete:"CompleteFile" desc:"Recover file path."`
}
func init() {
commands.Register(Recover{})
}
func (Recover) Description() string {
return "Resume composing a message that was not sent nor postponed."
}
func (Recover) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (Recover) Aliases() []string {
return []string{"recover"}
}
func (Recover) Options() string {
return "feE"
}
func (*Recover) CompleteFile(arg string) []string {
// file name of temp file is hard-coded in the NewComposer() function
files, err := filepath.Glob(
filepath.Join(os.TempDir(), "aerc-compose-*.eml"),
)
if err != nil {
return nil
}
return commands.FilterList(files, arg, nil)
}
func (r Recover) Execute(args []string) error {
file, err := os.Open(r.File)
if err != nil {
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit
composer, err := app.NewComposer(acct,
acct.AccountConfig(), acct.Worker(), editHeaders,
"", nil, nil, bytes.NewReader(data))
if err != nil {
return err
}
composer.Tab = app.NewTab(composer, "Recovered")
// remove file if force flag is set
if r.Force {
err = os.Remove(r.File)
if err != nil {
return err
}
}
return nil
}
+154
View File
@@ -0,0 +1,154 @@
package account
import (
"errors"
"fmt"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~rjarry/go-opt/v2"
)
type RemoveDir struct {
Force bool `opt:"-f" desc:"Remove the directory even if it contains messages."`
Folder string `opt:"folder" complete:"CompleteFolder" required:"false" desc:"Folder name."`
}
func init() {
commands.Register(RemoveDir{})
}
func (RemoveDir) Description() string {
return "Remove folder."
}
func (RemoveDir) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (RemoveDir) Aliases() []string {
return []string{"rmdir"}
}
func (RemoveDir) CompleteFolder(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
return commands.FilterList(acct.Directories().List(), arg, opt.QuoteArg)
}
func (r RemoveDir) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
current := acct.Directories().SelectedDirectory()
toRemove := current
if r.Folder != "" {
toRemove = acct.Directories().Directory(r.Folder)
if toRemove == nil {
return fmt.Errorf("No such directory: %s", r.Folder)
}
}
role := toRemove.Role
// Check for any messages in the directory.
if role != models.QueryRole && toRemove.Exists > 0 && !r.Force {
return errors.New("Refusing to remove non-empty directory; use -f")
}
if role == models.VirtualRole {
return errors.New("Cannot remove a virtual node")
}
if toRemove != current {
r.remove(acct, toRemove, func() {})
return nil
}
curDir := current.Name
var newDir string
dirFound := false
oldDir := acct.Directories().Previous()
if oldDir != "" {
present := false
for _, dir := range acct.Directories().List() {
if dir == oldDir {
present = true
break
}
}
if oldDir != curDir && present {
newDir = oldDir
dirFound = true
}
}
defaultDir := acct.AccountConfig().Default
if !dirFound && defaultDir != curDir {
for _, dir := range acct.Directories().List() {
if defaultDir == dir {
newDir = dir
dirFound = true
break
}
}
}
if !dirFound {
for _, dir := range acct.Directories().List() {
if dir != curDir {
newDir = dir
dirFound = true
break
}
}
}
if !dirFound {
return errors.New("No directory to move to afterwards!")
}
reopenCurrentDir := func() { acct.Directories().Open(curDir, "", 0, nil, false) }
acct.Directories().Open(newDir, "", 0, func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Done:
break
case *types.Error:
app.PushError("Could not change directory")
reopenCurrentDir()
return
default:
return
}
r.remove(acct, toRemove, reopenCurrentDir)
}, false)
return nil
}
func (r RemoveDir) remove(acct *app.AccountView, dir *models.Directory, onErr func()) {
acct.Worker().PostAction(&types.RemoveDirectory{
Directory: dir.Name,
Quiet: r.Force,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
app.PushStatus("Directory removed.", 10*time.Second)
case *types.Error:
app.PushError(msg.Error.Error())
onErr()
case *types.Unsupported:
app.PushError(":rmdir is not supported by the backend.")
onErr()
}
})
}
+223
View File
@@ -0,0 +1,223 @@
package account
import (
"errors"
"fmt"
"net/textproto"
"strings"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/parse"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/imap/extensions/xgmext"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type SearchFilter struct {
Read bool `opt:"-r" action:"ParseRead" desc:"Search for read messages."`
Unread bool `opt:"-u" action:"ParseUnread" desc:"Search for unread messages."`
Body bool `opt:"-b" desc:"Search in the body of the messages."`
All bool `opt:"-a" desc:"Search in the entire text of the messages."`
UseExtension bool `opt:"-e" desc:"Use custom search backend extension."`
Headers textproto.MIMEHeader `opt:"-H" action:"ParseHeader" metavar:"<header>:<value>" desc:"Search for messages with the specified header."`
WithFlags models.Flags `opt:"-x" action:"ParseFlag" complete:"CompleteFlag" desc:"Search messages with specified flag."`
WithoutFlags models.Flags `opt:"-X" action:"ParseNotFlag" complete:"CompleteFlag" desc:"Search messages without specified flag."`
To []string `opt:"-t" action:"ParseTo" complete:"CompleteAddress" desc:"Search for messages To:<address>."`
From []string `opt:"-f" action:"ParseFrom" complete:"CompleteAddress" desc:"Search for messages From:<address>."`
Cc []string `opt:"-c" action:"ParseCc" complete:"CompleteAddress" desc:"Search for messages Cc:<address>."`
StartDate time.Time `opt:"-d" action:"ParseDate" complete:"CompleteDate" desc:"Search for messages within a particular date range."`
EndDate time.Time
Terms string `opt:"..." required:"false" complete:"CompleteTerms" desc:"Search term."`
}
func init() {
commands.Register(SearchFilter{})
}
func (SearchFilter) Description() string {
return "Search or filter the current folder."
}
func (SearchFilter) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (SearchFilter) Aliases() []string {
return []string{"search", "filter"}
}
func (*SearchFilter) CompleteFlag(arg string) []string {
return commands.FilterList(commands.GetFlagList(), arg, commands.QuoteSpace)
}
func (*SearchFilter) CompleteAddress(arg string) []string {
return commands.FilterList(commands.GetAddress(arg), arg, commands.QuoteSpace)
}
func (*SearchFilter) CompleteDate(arg string) []string {
return commands.FilterList(commands.GetDateList(), arg, commands.QuoteSpace)
}
func (s *SearchFilter) CompleteTerms(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
if acct.AccountConfig().Backend == "notmuch" {
return handleNotmuchComplete(arg)
}
caps := acct.Worker().Backend.Capabilities()
if caps != nil && caps.Has("X-GM-EXT-1") && s.UseExtension {
return handleXGMEXTComplete(arg)
}
return nil
}
func (s *SearchFilter) ParseRead(arg string) error {
s.WithFlags |= models.SeenFlag
s.WithoutFlags &^= models.SeenFlag
return nil
}
func (s *SearchFilter) ParseUnread(arg string) error {
s.WithFlags &^= models.SeenFlag
s.WithoutFlags |= models.SeenFlag
return nil
}
var flagValues = map[string]models.Flags{
"seen": models.SeenFlag,
"answered": models.AnsweredFlag,
"forwarded": models.ForwardedFlag,
"flagged": models.FlaggedFlag,
"draft": models.DraftFlag,
}
func (s *SearchFilter) ParseFlag(arg string) error {
f, ok := flagValues[strings.ToLower(arg)]
if !ok {
return fmt.Errorf("%q unknown flag", arg)
}
s.WithFlags |= f
s.WithoutFlags &^= f
return nil
}
func (s *SearchFilter) ParseNotFlag(arg string) error {
f, ok := flagValues[strings.ToLower(arg)]
if !ok {
return fmt.Errorf("%q unknown flag", arg)
}
s.WithFlags &^= f
s.WithoutFlags |= f
return nil
}
func (s *SearchFilter) ParseHeader(arg string) error {
name, value, hasColon := strings.Cut(arg, ":")
if !hasColon {
return fmt.Errorf("%q invalid syntax", arg)
}
if s.Headers == nil {
s.Headers = make(textproto.MIMEHeader)
}
s.Headers.Add(name, strings.TrimSpace(value))
return nil
}
func (s *SearchFilter) ParseTo(arg string) error {
s.To = append(s.To, arg)
return nil
}
func (s *SearchFilter) ParseFrom(arg string) error {
s.From = append(s.From, arg)
return nil
}
func (s *SearchFilter) ParseCc(arg string) error {
s.Cc = append(s.Cc, arg)
return nil
}
func (s *SearchFilter) ParseDate(arg string) error {
start, end, err := parse.DateRange(arg)
if err != nil {
return err
}
s.StartDate = start
s.EndDate = end
return nil
}
func (s SearchFilter) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := acct.Store()
if store == nil {
return errors.New("Cannot perform action. Messages still loading")
}
criteria := types.SearchCriteria{
WithFlags: s.WithFlags,
WithoutFlags: s.WithoutFlags,
From: s.From,
To: s.To,
Cc: s.Cc,
Headers: s.Headers,
StartDate: s.StartDate,
EndDate: s.EndDate,
SearchBody: s.Body,
SearchAll: s.All,
Terms: []string{s.Terms},
UseExtension: s.UseExtension,
}
if args[0] == "filter" {
if len(args[1:]) == 0 {
return Clear{}.Execute([]string{"clear"})
}
acct.SetStatus(state.FilterActivity("Filtering..."), state.Search(""))
store.SetFilter(&criteria)
cb := func(msg types.WorkerMessage) {
if _, ok := msg.(*types.Done); ok {
acct.SetStatus(state.FilterResult(strings.Join(args, " ")))
log.Tracef("Filter results: %v", store.Uids())
}
}
store.Sort(store.GetCurrentSortCriteria(), cb)
} else {
acct.SetStatus(state.Search("Searching..."))
cb := func(uids []models.UID) {
acct.SetStatus(state.Search(strings.Join(args, " ")))
log.Tracef("Search results: %v", uids)
store.ApplySearch(uids)
// TODO: Remove when stores have multiple OnUpdate handlers
ui.Invalidate()
}
store.Search(&criteria, cb)
}
return nil
}
func handleXGMEXTComplete(arg string) []string {
prefixes := []string{"from:", "to:", "deliveredto:", "cc:", "bcc:"}
for _, prefix := range prefixes {
if strings.HasPrefix(arg, prefix) {
arg = strings.TrimPrefix(arg, prefix)
return commands.FilterList(
commands.GetAddress(arg), arg,
func(v string) string { return prefix + v },
)
}
}
return commands.FilterList(xgmext.Terms, arg, nil)
}
+40
View File
@@ -0,0 +1,40 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type SelectMessage struct {
Index int `opt:"n"`
}
func init() {
commands.Register(SelectMessage{})
}
func (SelectMessage) Description() string {
return "Select the <N>th message in the message list."
}
func (SelectMessage) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (SelectMessage) Aliases() []string {
return []string{"select", "select-message"}
}
func (s SelectMessage) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
if acct.Messages().Empty() {
return nil
}
acct.Messages().Select(s.Index)
return nil
}
+86
View File
@@ -0,0 +1,86 @@
package account
import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type Sort struct {
Unused struct{} `opt:"-"`
// these fields are only used for completion
Reverse bool `opt:"-r" desc:"Sort in the reverse order."`
Criteria []string `opt:"criteria" complete:"CompleteCriteria" desc:"Sort criterion."`
}
func init() {
commands.Register(Sort{})
}
func (Sort) Description() string {
return "Sort the message list by the given criteria."
}
func (Sort) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (Sort) Aliases() []string {
return []string{"sort"}
}
var supportedCriteria = []string{
"arrival",
"cc",
"date",
"from",
"read",
"size",
"subject",
"to",
"flagged",
}
func (*Sort) CompleteCriteria(arg string) []string {
return commands.FilterList(supportedCriteria, arg, commands.QuoteSpace)
}
func (Sort) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected.")
}
store := acct.Store()
if store == nil {
return errors.New("Messages still loading.")
}
if c := store.Capabilities(); c != nil {
if !c.Sort {
return errors.New("Sorting is not available for this backend.")
}
}
var err error
var sortCriteria []*types.SortCriterion
if len(args[1:]) == 0 {
sortCriteria = acct.GetSortCriteria()
} else {
sortCriteria, err = sort.GetSortCriteria(args[1:])
if err != nil {
return err
}
}
acct.SetStatus(state.Sorting(true))
store.Sort(sortCriteria, func(msg types.WorkerMessage) {
if _, ok := msg.(*types.Done); ok {
acct.SetStatus(state.Sorting(false))
}
})
return nil
}
+82
View File
@@ -0,0 +1,82 @@
package account
import (
"errors"
"strconv"
"strings"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type Split struct {
Size int `opt:"n" required:"false" action:"ParseSize"`
Delta bool
}
func init() {
commands.Register(Split{})
}
func (Split) Description() string {
return "Split the message list with a preview pane."
}
func (Split) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (s *Split) ParseSize(arg string) error {
i, err := strconv.ParseInt(arg, 10, 64)
if err != nil {
return err
}
s.Size = int(i)
if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") {
s.Delta = true
}
return nil
}
func (Split) Aliases() []string {
return []string{"split", "vsplit", "hsplit"}
}
func (s Split) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
store := app.SelectedAccount().Store()
if store == nil {
return errors.New("Cannot perform action. Messages still loading")
}
if s.Size == 0 && acct.SplitSize() == 0 {
if args[0] == "split" || args[0] == "hsplit" {
s.Size = app.SelectedAccount().Messages().Height() / 4
} else {
s.Size = app.SelectedAccount().Messages().Width() / 2
}
}
if s.Delta {
acct.SetSplitSize(acct.SplitSize() + s.Size)
return nil
}
if s.Size == acct.SplitSize() {
// Repeated commands of the same size have the effect of
// toggling the split
s.Size = 0
}
if s.Size < 0 {
// Don't allow split to go negative
s.Size = 1
}
switch args[0] {
case "split", "hsplit":
acct.Split(s.Size)
case "vsplit":
acct.Vsplit(s.Size)
}
return nil
}
+91
View File
@@ -0,0 +1,91 @@
package account
import (
"bytes"
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/models"
)
type ViewMessage struct {
Peek bool `opt:"-p" desc:"Peek message without marking it as read."`
Background bool `opt:"-b" desc:"Open message in a background tab."`
}
func init() {
commands.Register(ViewMessage{})
}
func (ViewMessage) Description() string {
return "View the selected message in a new tab."
}
func (ViewMessage) Context() commands.CommandContext {
return commands.MESSAGE_LIST
}
func (ViewMessage) Aliases() []string {
return []string{"view-message", "view"}
}
func (v ViewMessage) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
if acct.Messages().Empty() {
return nil
}
store := acct.Messages().Store()
msg := acct.Messages().Selected()
if msg == nil {
return nil
}
_, deleted := store.Deleted[msg.Uid]
if deleted {
return nil
}
if msg.Error != nil {
app.PushError(msg.Error.Error())
return nil
}
lib.NewMessageStoreView(
msg,
!v.Peek && acct.UiConfig().AutoMarkRead,
store,
app.CryptoProvider(),
app.DecryptKeys,
func(view lib.MessageView, err error) {
if err != nil {
app.PushError(err.Error())
return
}
viewer, err := app.NewMessageViewer(acct, view)
if err != nil {
app.PushError(err.Error())
return
}
data := state.NewDataSetter()
data.SetAccount(acct.AccountConfig())
data.SetFolder(acct.Directories().SelectedDirectory())
data.SetHeaders(msg.RFC822Headers, &models.OriginalMail{})
var buf bytes.Buffer
err = templates.Render(acct.UiConfig().TabTitleViewer, &buf,
data.Data())
if err != nil {
acct.PushError(err)
return
}
if v.Background {
app.NewBackgroundTab(viewer, buf.String())
} else {
app.NewTab(viewer, buf.String())
}
})
return nil
}