aboutsummaryrefslogtreecommitdiffstats
path: root/app/aerc.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/aerc.go')
-rw-r--r--app/aerc.go908
1 files changed, 908 insertions, 0 deletions
diff --git a/app/aerc.go b/app/aerc.go
new file mode 100644
index 00000000..a1995c2d
--- /dev/null
+++ b/app/aerc.go
@@ -0,0 +1,908 @@
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/url"
+ "os/exec"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/ProtonMail/go-crypto/openpgp"
+ "github.com/emersion/go-message/mail"
+ "github.com/gdamore/tcell/v2"
+ "github.com/google/shlex"
+
+ "git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib"
+ "git.sr.ht/~rjarry/aerc/lib/crypto"
+ "git.sr.ht/~rjarry/aerc/lib/ui"
+ "git.sr.ht/~rjarry/aerc/log"
+ "git.sr.ht/~rjarry/aerc/models"
+ "git.sr.ht/~rjarry/aerc/worker/types"
+)
+
+type Aerc struct {
+ accounts map[string]*AccountView
+ cmd func([]string, *config.AccountConfig, *models.MessageInfo) error
+ cmdHistory lib.History
+ complete func(cmd string) ([]string, string)
+ focused ui.Interactive
+ grid *ui.Grid
+ simulating int
+ statusbar *ui.Stack
+ statusline *StatusLine
+ pasting bool
+ pendingKeys []config.KeyStroke
+ prompts *ui.Stack
+ tabs *ui.Tabs
+ ui *ui.UI
+ beep func() error
+ dialog ui.DrawableInteractive
+
+ Crypto crypto.Provider
+}
+
+type Choice struct {
+ Key string
+ Text string
+ Command []string
+}
+
+func NewAerc(
+ crypto crypto.Provider,
+ cmd func([]string, *config.AccountConfig, *models.MessageInfo) error,
+ complete func(cmd string) ([]string, string), cmdHistory lib.History,
+ deferLoop chan struct{},
+) *Aerc {
+ tabs := ui.NewTabs(config.Ui)
+
+ statusbar := ui.NewStack(config.Ui)
+ statusline := &StatusLine{}
+ statusbar.Push(statusline)
+
+ grid := ui.NewGrid().Rows([]ui.GridSpec{
+ {Strategy: ui.SIZE_EXACT, Size: ui.Const(1)},
+ {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
+ {Strategy: ui.SIZE_EXACT, Size: ui.Const(1)},
+ }).Columns([]ui.GridSpec{
+ {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
+ })
+ grid.AddChild(tabs.TabStrip)
+ grid.AddChild(tabs.TabContent).At(1, 0)
+ grid.AddChild(statusbar).At(2, 0)
+
+ aerc := &Aerc{
+ accounts: make(map[string]*AccountView),
+ cmd: cmd,
+ cmdHistory: cmdHistory,
+ complete: complete,
+ grid: grid,
+ statusbar: statusbar,
+ statusline: statusline,
+ prompts: ui.NewStack(config.Ui),
+ tabs: tabs,
+ Crypto: crypto,
+ }
+
+ statusline.SetAerc(aerc)
+
+ for _, acct := range config.Accounts {
+ view, err := NewAccountView(aerc, acct, aerc, deferLoop)
+ if err != nil {
+ tabs.Add(errorScreen(err.Error()), acct.Name, nil)
+ } else {
+ aerc.accounts[acct.Name] = view
+ view.tab = tabs.Add(view, acct.Name, view.UiConfig())
+ }
+ }
+
+ if len(config.Accounts) == 0 {
+ wizard := NewAccountWizard(aerc)
+ wizard.Focus(true)
+ aerc.NewTab(wizard, "New account")
+ }
+
+ tabs.Select(0)
+
+ tabs.CloseTab = func(index int) {
+ tab := aerc.tabs.Get(index)
+ if tab == nil {
+ return
+ }
+ switch content := tab.Content.(type) {
+ case *AccountView:
+ return
+ case *AccountWizard:
+ return
+ default:
+ aerc.RemoveTab(content, true)
+ }
+ }
+
+ aerc.showConfigWarnings()
+
+ return aerc
+}
+
+func (aerc *Aerc) showConfigWarnings() {
+ var dialogs []ui.DrawableInteractive
+
+ callback := func(string, error) {
+ aerc.CloseDialog()
+ if len(dialogs) > 0 {
+ d := dialogs[0]
+ dialogs = dialogs[1:]
+ aerc.AddDialog(d)
+ }
+ }
+
+ for _, w := range config.Warnings {
+ dialogs = append(dialogs, NewSelectorDialog(
+ w.Title, w.Body, []string{"OK"}, 0,
+ aerc.SelectedAccountUiConfig(),
+ callback,
+ ))
+ }
+
+ callback("", nil)
+}
+
+func (aerc *Aerc) OnBeep(f func() error) {
+ aerc.beep = f
+}
+
+func (aerc *Aerc) Beep() {
+ if aerc.beep == nil {
+ log.Warnf("should beep, but no beeper")
+ return
+ }
+ if err := aerc.beep(); err != nil {
+ log.Errorf("tried to beep, but could not: %v", err)
+ }
+}
+
+func (aerc *Aerc) HandleMessage(msg types.WorkerMessage) {
+ if acct, ok := aerc.accounts[msg.Account()]; ok {
+ acct.onMessage(msg)
+ }
+}
+
+func (aerc *Aerc) Invalidate() {
+ ui.Invalidate()
+}
+
+func (aerc *Aerc) Focus(focus bool) {
+ // who cares
+}
+
+func (aerc *Aerc) Draw(ctx *ui.Context) {
+ if len(aerc.prompts.Children()) > 0 {
+ previous := aerc.focused
+ prompt := aerc.prompts.Pop().(*ExLine)
+ prompt.finish = func() {
+ aerc.statusbar.Pop()
+ aerc.focus(previous)
+ }
+
+ aerc.statusbar.Push(prompt)
+ aerc.focus(prompt)
+ }
+ aerc.grid.Draw(ctx)
+ if aerc.dialog != nil {
+ if w, h := ctx.Width(), ctx.Height(); w > 8 && h > 4 {
+ if d, ok := aerc.dialog.(Dialog); ok {
+ start, height := d.ContextHeight()
+ aerc.dialog.Draw(
+ ctx.Subcontext(4, start(h),
+ w-8, height(h)))
+ } else {
+ aerc.dialog.Draw(ctx.Subcontext(4, h/2-2, w-8, 4))
+ }
+ }
+ }
+}
+
+func (aerc *Aerc) HumanReadableBindings() []string {
+ var result []string
+ binds := aerc.getBindings()
+ format := func(s string) string {
+ return strings.ReplaceAll(s, "%", "%%")
+ }
+ fmtStr := "%10s %s"
+ for _, bind := range binds.Bindings {
+ result = append(result, fmt.Sprintf(fmtStr,
+ format(config.FormatKeyStrokes(bind.Input)),
+ format(config.FormatKeyStrokes(bind.Output)),
+ ))
+ }
+ if binds.Globals && config.Binds.Global != nil {
+ for _, bind := range config.Binds.Global.Bindings {
+ result = append(result, fmt.Sprintf(fmtStr+" (Globals)",
+ format(config.FormatKeyStrokes(bind.Input)),
+ format(config.FormatKeyStrokes(bind.Output)),
+ ))
+ }
+ }
+ result = append(result, fmt.Sprintf(fmtStr,
+ "$ex",
+ fmt.Sprintf("'%c'", binds.ExKey.Rune),
+ ))
+ result = append(result, fmt.Sprintf(fmtStr,
+ "Globals",
+ fmt.Sprintf("%v", binds.Globals),
+ ))
+ sort.Strings(result)
+ return result
+}
+
+func (aerc *Aerc) getBindings() *config.KeyBindings {
+ selectedAccountName := ""
+ if aerc.SelectedAccount() != nil {
+ selectedAccountName = aerc.SelectedAccount().acct.Name
+ }
+ switch view := aerc.SelectedTabContent().(type) {
+ case *AccountView:
+ binds := config.Binds.MessageList.ForAccount(selectedAccountName)
+ return binds.ForFolder(view.SelectedDirectory())
+ case *AccountWizard:
+ return config.Binds.AccountWizard
+ case *Composer:
+ switch view.Bindings() {
+ case "compose::editor":
+ return config.Binds.ComposeEditor.ForAccount(
+ selectedAccountName)
+ case "compose::review":
+ return config.Binds.ComposeReview.ForAccount(
+ selectedAccountName)
+ default:
+ return config.Binds.Compose.ForAccount(
+ selectedAccountName)
+ }
+ case *MessageViewer:
+ switch view.Bindings() {
+ case "view::passthrough":
+ return config.Binds.MessageViewPassthrough.ForAccount(
+ selectedAccountName)
+ default:
+ return config.Binds.MessageView.ForAccount(
+ selectedAccountName)
+ }
+ case *Terminal:
+ return config.Binds.Terminal
+ default:
+ return config.Binds.Global
+ }
+}
+
+func (aerc *Aerc) simulate(strokes []config.KeyStroke) {
+ aerc.pendingKeys = []config.KeyStroke{}
+ aerc.simulating += 1
+ for _, stroke := range strokes {
+ simulated := tcell.NewEventKey(
+ stroke.Key, stroke.Rune, tcell.ModNone)
+ aerc.Event(simulated)
+ }
+ aerc.simulating -= 1
+ // If we are still focused on the exline, turn on tab complete
+ if exline, ok := aerc.focused.(*ExLine); ok {
+ exline.TabComplete(func(cmd string) ([]string, string) {
+ return aerc.complete(cmd)
+ })
+ // send tab to text input to trigger completion
+ exline.Event(tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone))
+ }
+}
+
+func (aerc *Aerc) Event(event tcell.Event) bool {
+ if aerc.dialog != nil {
+ return aerc.dialog.Event(event)
+ }
+
+ if aerc.focused != nil {
+ return aerc.focused.Event(event)
+ }
+
+ switch event := event.(type) {
+ case *tcell.EventKey:
+ // If we are in a bracketed paste, don't process the keys for
+ // bindings
+ if aerc.pasting {
+ interactive, ok := aerc.SelectedTabContent().(ui.Interactive)
+ if ok {
+ return interactive.Event(event)
+ }
+ return false
+ }
+ aerc.statusline.Expire()
+ aerc.pendingKeys = append(aerc.pendingKeys, config.KeyStroke{
+ Modifiers: event.Modifiers(),
+ Key: event.Key(),
+ Rune: event.Rune(),
+ })
+ ui.Invalidate()
+ bindings := aerc.getBindings()
+ incomplete := false
+ result, strokes := bindings.GetBinding(aerc.pendingKeys)
+ switch result {
+ case config.BINDING_FOUND:
+ aerc.simulate(strokes)
+ return true
+ case config.BINDING_INCOMPLETE:
+ incomplete = true
+ case config.BINDING_NOT_FOUND:
+ }
+ if bindings.Globals {
+ result, strokes = config.Binds.Global.GetBinding(aerc.pendingKeys)
+ switch result {
+ case config.BINDING_FOUND:
+ aerc.simulate(strokes)
+ return true
+ case config.BINDING_INCOMPLETE:
+ incomplete = true
+ case config.BINDING_NOT_FOUND:
+ }
+ }
+ if !incomplete {
+ aerc.pendingKeys = []config.KeyStroke{}
+ exKey := bindings.ExKey
+ if aerc.simulating > 0 {
+ // Keybindings still use : even if you change the ex key
+ exKey = config.Binds.Global.ExKey
+ }
+ if aerc.isExKey(event, exKey) {
+ aerc.BeginExCommand("")
+ return true
+ }
+ interactive, ok := aerc.SelectedTabContent().(ui.Interactive)
+ if ok {
+ return interactive.Event(event)
+ }
+ return false
+ }
+ case *tcell.EventMouse:
+ x, y := event.Position()
+ aerc.grid.MouseEvent(x, y, event)
+ return true
+ case *tcell.EventPaste:
+ if event.Start() {
+ aerc.pasting = true
+ }
+ if event.End() {
+ aerc.pasting = false
+ }
+ interactive, ok := aerc.SelectedTabContent().(ui.Interactive)
+ if ok {
+ return interactive.Event(event)
+ }
+ return false
+ }
+ return false
+}
+
+func (aerc *Aerc) SelectedAccount() *AccountView {
+ return aerc.account(aerc.SelectedTabContent())
+}
+
+func (aerc *Aerc) Account(name string) (*AccountView, error) {
+ if acct, ok := aerc.accounts[name]; ok {
+ return acct, nil
+ }
+ return nil, fmt.Errorf("account <%s> not found", name)
+}
+
+func (aerc *Aerc) PrevAccount() (*AccountView, error) {
+ cur := aerc.SelectedAccount()
+ if cur == nil {
+ return nil, fmt.Errorf("no account selected, cannot get prev")
+ }
+ for i, conf := range config.Accounts {
+ if conf.Name == cur.Name() {
+ i -= 1
+ if i == -1 {
+ i = len(config.Accounts) - 1
+ }
+ conf = config.Accounts[i]
+ return aerc.Account(conf.Name)
+ }
+ }
+ return nil, fmt.Errorf("no prev account")
+}
+
+func (aerc *Aerc) NextAccount() (*AccountView, error) {
+ cur := aerc.SelectedAccount()
+ if cur == nil {
+ return nil, fmt.Errorf("no account selected, cannot get next")
+ }
+ for i, conf := range config.Accounts {
+ if conf.Name == cur.Name() {
+ i += 1
+ if i == len(config.Accounts) {
+ i = 0
+ }
+ conf = config.Accounts[i]
+ return aerc.Account(conf.Name)
+ }
+ }
+ return nil, fmt.Errorf("no next account")
+}
+
+func (aerc *Aerc) AccountNames() []string {
+ results := make([]string, 0)
+ for name := range aerc.accounts {
+ results = append(results, name)
+ }
+ return results
+}
+
+func (aerc *Aerc) account(d ui.Drawable) *AccountView {
+ switch tab := d.(type) {
+ case *AccountView:
+ return tab
+ case *MessageViewer:
+ return tab.SelectedAccount()
+ case *Composer:
+ return tab.Account()
+ }
+ return nil
+}
+
+func (aerc *Aerc) SelectedAccountUiConfig() *config.UIConfig {
+ acct := aerc.SelectedAccount()
+ if acct == nil {
+ return config.Ui
+ }
+ return acct.UiConfig()
+}
+
+func (aerc *Aerc) SelectedTabContent() ui.Drawable {
+ tab := aerc.tabs.Selected()
+ if tab == nil {
+ return nil
+ }
+ return tab.Content
+}
+
+func (aerc *Aerc) SelectedTab() *ui.Tab {
+ return aerc.tabs.Selected()
+}
+
+func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab {
+ uiConf := config.Ui
+ if acct := aerc.account(clickable); acct != nil {
+ uiConf = acct.UiConfig()
+ }
+ tab := aerc.tabs.Add(clickable, name, uiConf)
+ aerc.UpdateStatus()
+ return tab
+}
+
+func (aerc *Aerc) RemoveTab(tab ui.Drawable, closeContent bool) {
+ aerc.tabs.Remove(tab)
+ aerc.UpdateStatus()
+ if content, ok := tab.(ui.Closeable); ok && closeContent {
+ content.Close()
+ }
+}
+
+func (aerc *Aerc) ReplaceTab(tabSrc ui.Drawable, tabTarget ui.Drawable, name string, closeSrc bool) {
+ aerc.tabs.Replace(tabSrc, tabTarget, name)
+ if content, ok := tabSrc.(ui.Closeable); ok && closeSrc {
+ content.Close()
+ }
+}
+
+func (aerc *Aerc) MoveTab(i int, relative bool) {
+ aerc.tabs.MoveTab(i, relative)
+}
+
+func (aerc *Aerc) PinTab() {
+ aerc.tabs.PinTab()
+}
+
+func (aerc *Aerc) UnpinTab() {
+ aerc.tabs.UnpinTab()
+}
+
+func (aerc *Aerc) NextTab() {
+ aerc.tabs.NextTab()
+}
+
+func (aerc *Aerc) PrevTab() {
+ aerc.tabs.PrevTab()
+}
+
+func (aerc *Aerc) SelectTab(name string) bool {
+ ok := aerc.tabs.SelectName(name)
+ if ok {
+ aerc.UpdateStatus()
+ }
+ return ok
+}
+
+func (aerc *Aerc) SelectTabIndex(index int) bool {
+ ok := aerc.tabs.Select(index)
+ if ok {
+ aerc.UpdateStatus()
+ }
+ return ok
+}
+
+func (aerc *Aerc) TabNames() []string {
+ return aerc.tabs.Names()
+}
+
+func (aerc *Aerc) SelectPreviousTab() bool {
+ return aerc.tabs.SelectPrevious()
+}
+
+func (aerc *Aerc) UpdateStatus() {
+ if acct := aerc.SelectedAccount(); acct != nil {
+ aerc.statusline.Update(acct)
+ } else {
+ aerc.statusline.Clear()
+ }
+}
+
+func (aerc *Aerc) SetError(err string) {
+ aerc.statusline.SetError(err)
+}
+
+func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
+ return aerc.statusline.Push(text, expiry)
+}
+
+func (aerc *Aerc) PushError(text string) *StatusMessage {
+ return aerc.statusline.PushError(text)
+}
+
+func (aerc *Aerc) PushWarning(text string) *StatusMessage {
+ return aerc.statusline.PushWarning(text)
+}
+
+func (aerc *Aerc) PushSuccess(text string) *StatusMessage {
+ return aerc.statusline.PushSuccess(text)
+}
+
+func (aerc *Aerc) focus(item ui.Interactive) {
+ if aerc.focused == item {
+ return
+ }
+ if aerc.focused != nil {
+ aerc.focused.Focus(false)
+ }
+ aerc.focused = item
+ interactive, ok := aerc.SelectedTabContent().(ui.Interactive)
+ if item != nil {
+ item.Focus(true)
+ if ok {
+ interactive.Focus(false)
+ }
+ } else if ok {
+ interactive.Focus(true)
+ }
+}
+
+func (aerc *Aerc) BeginExCommand(cmd string) {
+ previous := aerc.focused
+ var tabComplete func(string) ([]string, string)
+ if aerc.simulating != 0 {
+ // Don't try to draw completions for simulated events
+ tabComplete = nil
+ } else {
+ tabComplete = func(cmd string) ([]string, string) {
+ return aerc.complete(cmd)
+ }
+ }
+ exline := NewExLine(cmd, func(cmd string) {
+ parts, err := shlex.Split(cmd)
+ if err != nil {
+ aerc.PushError(err.Error())
+ }
+ err = aerc.cmd(parts, nil, nil)
+ if err != nil {
+ aerc.PushError(err.Error())
+ }
+ // only add to history if this is an unsimulated command,
+ // ie one not executed from a keybinding
+ if aerc.simulating == 0 {
+ aerc.cmdHistory.Add(cmd)
+ }
+ }, func() {
+ aerc.statusbar.Pop()
+ aerc.focus(previous)
+ }, tabComplete, aerc.cmdHistory)
+ aerc.statusbar.Push(exline)
+ aerc.focus(exline)
+}
+
+func (aerc *Aerc) PushPrompt(prompt *ExLine) {
+ aerc.prompts.Push(prompt)
+}
+
+func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
+ p := NewPrompt(prompt, func(text string) {
+ if text != "" {
+ cmd = append(cmd, text)
+ }
+ err := aerc.cmd(cmd, nil, nil)
+ if err != nil {
+ aerc.PushError(err.Error())
+ }
+ }, func(cmd string) ([]string, string) {
+ return nil, "" // TODO: completions
+ })
+ aerc.prompts.Push(p)
+}
+
+func (aerc *Aerc) RegisterChoices(choices []Choice) {
+ cmds := make(map[string][]string)
+ texts := []string{}
+ for _, c := range choices {
+ text := fmt.Sprintf("[%s] %s", c.Key, c.Text)
+ if strings.Contains(c.Text, c.Key) {
+ text = strings.Replace(c.Text, c.Key, "["+c.Key+"]", 1)
+ }
+ texts = append(texts, text)
+ cmds[c.Key] = c.Command
+ }
+ prompt := strings.Join(texts, ", ") + "? "
+ p := NewPrompt(prompt, func(text string) {
+ cmd, ok := cmds[text]
+ if !ok {
+ return
+ }
+ err := aerc.cmd(cmd, nil, nil)
+ if err != nil {
+ aerc.PushError(err.Error())
+ }
+ }, func(cmd string) ([]string, string) {
+ return nil, "" // TODO: completions
+ })
+ aerc.prompts.Push(p)
+}
+
+func (aerc *Aerc) Mailto(addr *url.URL) error {
+ var subject string
+ var body string
+ var acctName string
+ var attachments []string
+ h := &mail.Header{}
+ to, err := mail.ParseAddressList(addr.Opaque)
+ if err != nil && addr.Opaque != "" {
+ return fmt.Errorf("Could not parse to: %w", err)
+ }
+ h.SetAddressList("to", to)
+ template := config.Templates.NewMessage
+ for key, vals := range addr.Query() {
+ switch strings.ToLower(key) {
+ case "account":
+ acctName = strings.Join(vals, "")
+ case "bcc":
+ list, err := mail.ParseAddressList(strings.Join(vals, ","))
+ if err != nil {
+ break
+ }
+ h.SetAddressList("Bcc", list)
+ case "body":
+ body = strings.Join(vals, "\n")
+ case "cc":
+ list, err := mail.ParseAddressList(strings.Join(vals, ","))
+ if err != nil {
+ break
+ }
+ h.SetAddressList("Cc", list)
+ case "in-reply-to":
+ for i, msgID := range vals {
+ if len(msgID) > 1 && msgID[0] == '<' &&
+ msgID[len(msgID)-1] == '>' {
+ vals[i] = msgID[1 : len(msgID)-1]
+ }
+ }
+ h.SetMsgIDList("In-Reply-To", vals)
+ case "subject":
+ subject = strings.Join(vals, ",")
+ h.SetText("Subject", subject)
+ case "template":
+ template = strings.Join(vals, "")
+ log.Tracef("template set to %s", template)
+ case "attach":
+ for _, path := range vals {
+ // remove a potential file:// prefix.
+ attachments = append(attachments, strings.TrimPrefix(path, "file://"))
+ }
+ default:
+ // any other header gets ignored on purpose to avoid control headers
+ // being injected
+ }
+ }
+
+ acct := aerc.SelectedAccount()
+ if acctName != "" {
+ if a, ok := aerc.accounts[acctName]; ok && a != nil {
+ acct = a
+ }
+ }
+
+ if acct == nil {
+ return errors.New("No account selected")
+ }
+
+ defer ui.Invalidate()
+
+ composer, err := NewComposer(aerc, acct,
+ acct.AccountConfig(), acct.Worker(),
+ config.Compose.EditHeaders, template, h, nil,
+ strings.NewReader(body))
+ if err != nil {
+ return err
+ }
+ composer.FocusEditor("subject")
+ title := "New email"
+ if subject != "" {
+ title = subject
+ composer.FocusTerminal()
+ }
+ if to == nil {
+ composer.FocusEditor("to")
+ }
+ composer.Tab = aerc.NewTab(composer, title)
+
+ for _, file := range attachments {
+ composer.AddAttachment(file)
+ }
+ return nil
+}
+
+func (aerc *Aerc) Mbox(source string) error {
+ acctConf := config.AccountConfig{}
+ if selectedAcct := aerc.SelectedAccount(); selectedAcct != nil {
+ acctConf = *selectedAcct.acct
+ info := fmt.Sprintf("Loading outgoing mbox mail settings from account [%s]", selectedAcct.Name())
+ aerc.PushStatus(info, 10*time.Second)
+ log.Debugf(info)
+ } else {
+ acctConf.From = &mail.Address{Address: "user@localhost"}
+ }
+ acctConf.Name = "mbox"
+ acctConf.Source = source
+ acctConf.Default = "INBOX"
+ acctConf.Archive = "Archive"
+ acctConf.Postpone = "Drafts"
+ acctConf.CopyTo = "Sent"
+
+ defer ui.Invalidate()
+
+ mboxView, err := NewAccountView(aerc, &acctConf, aerc, nil)
+ if err != nil {
+ aerc.NewTab(errorScreen(err.Error()), acctConf.Name)
+ } else {
+ aerc.accounts[acctConf.Name] = mboxView
+ aerc.NewTab(mboxView, acctConf.Name)
+ }
+ return nil
+}
+
+func (aerc *Aerc) Command(args []string) error {
+ defer ui.Invalidate()
+ return aerc.cmd(args, nil, nil)
+}
+
+func (aerc *Aerc) CloseBackends() error {
+ var returnErr error
+ for _, acct := range aerc.accounts {
+ var raw interface{} = acct.worker.Backend
+ c, ok := raw.(io.Closer)
+ if !ok {
+ continue
+ }
+ err := c.Close()
+ if err != nil {
+ returnErr = err
+ log.Errorf("Closing backend failed for %s: %v", acct.Name(), err)
+ }
+ }
+ return returnErr
+}
+
+func (aerc *Aerc) AddDialog(d ui.DrawableInteractive) {
+ aerc.dialog = d
+ aerc.Invalidate()
+}
+
+func (aerc *Aerc) CloseDialog() {
+ aerc.dialog = nil
+ aerc.Invalidate()
+}
+
+func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) {
+ chText = make(chan string, 1)
+ chErr = make(chan error, 1)
+ getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) {
+ defer func() {
+ close(chErr)
+ close(chText)
+ aerc.CloseDialog()
+ }()
+ if err != nil {
+ chErr <- err
+ return
+ }
+ chErr <- nil
+ chText <- pw
+ })
+ aerc.AddDialog(getPasswd)
+
+ return
+}
+
+func (aerc *Aerc) Initialize(ui *ui.UI) {
+ aerc.ui = ui
+}
+
+func (aerc *Aerc) DecryptKeys(keys []openpgp.Key, symmetric bool) (b []byte, err error) {
+ for _, key := range keys {
+ ident := key.Entity.PrimaryIdentity()
+ chPass, chErr := aerc.GetPassword("Decrypt PGP private key",
+ fmt.Sprintf("Enter password for %s (%8X)\nPress <ESC> to cancel",
+ ident.Name, key.PublicKey.KeyId))
+
+ for err := range chErr {
+ if err != nil {
+ return nil, err
+ }
+ pass := <-chPass
+ err = key.PrivateKey.Decrypt([]byte(pass))
+ return nil, err
+ }
+ }
+ return nil, err
+}
+
+// errorScreen is a widget that draws an error in the middle of the context
+func errorScreen(s string) ui.Drawable {
+ errstyle := config.Ui.GetStyle(config.STYLE_ERROR)
+ text := ui.NewText(s, errstyle).Strategy(ui.TEXT_CENTER)
+ grid := ui.NewGrid().Rows([]ui.GridSpec{
+ {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
+ {Strategy: ui.SIZE_EXACT, Size: ui.Const(1)},
+ {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
+ }).Columns([]ui.GridSpec{
+ {Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
+ })
+ grid.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(0, 0)
+ grid.AddChild(text).At(1, 0)
+ grid.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(2, 0)
+ return grid
+}
+
+func (aerc *Aerc) isExKey(event *tcell.EventKey, exKey config.KeyStroke) bool {
+ if event.Key() == tcell.KeyRune {
+ // Compare runes if it's a KeyRune
+ return event.Modifiers() == exKey.Modifiers && event.Rune() == exKey.Rune
+ }
+ return event.Modifiers() == exKey.Modifiers && event.Key() == exKey.Key
+}
+
+// CmdFallbackSearch checks cmds for the first executable availabe in PATH. An error is
+// returned if none are found
+func (aerc *Aerc) CmdFallbackSearch(cmds []string) (string, error) {
+ var tried []string
+ for _, cmd := range cmds {
+ if cmd == "" {
+ continue
+ }
+ params := strings.Split(cmd, " ")
+ _, err := exec.LookPath(params[0])
+ if err != nil {
+ tried = append(tried, cmd)
+ warn := fmt.Sprintf("cmd '%s' not found in PATH, using fallback", cmd)
+ aerc.PushWarning(warn)
+ continue
+ }
+ return cmd, nil
+ }
+ return "", fmt.Errorf("no command found in PATH: %s", tried)
+}