1
0
Fork 0
mirror of https://git.sr.ht/~rjarry/aerc synced 2026-01-04 20:31:11 +01:00
aerc/app/aerc.go
Robin Jarry 51fd25c0f1 reload: fix crash when reloading via IPC
When reloading the configuration with :reload, global variables in the
config package are reset to their startup values and then, the config is
parsed from disk. While the parsing is done, these variables are
temporarily in an inconsistent and possibly invalid state.

When commands are executed interactively from aerc, they are handled by
the main goroutine which also deals with UI rendering. No UI render will
be done while :reload is in progress.

However, the IPC socket handler runs in an independent goroutine. This
has the unfortunate side effect to let the UI goroutine to run while
config parsing is in progress and causes crashes:

[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6bb142]

goroutine 1 [running]:
git.sr.ht/~rjarry/aerc/lib/log.PanicHandler()
	lib/log/panic-logger.go:51 +0x6cf
panic({0xc1d960?, 0x134a6e0?})
	/usr/lib/go/src/runtime/panic.go:783 +0x132
git.sr.ht/~rjarry/aerc/config.(*StyleConf).getStyle(0xc00038b908?, 0x4206b7?)
	config/style.go:386 +0x42
git.sr.ht/~rjarry/aerc/config.StyleSet.Get({0x0, 0x0, 0x0, {0x0, 0x0, 0x0}}, 0x421a65?, 0x0)
	config/style.go:408 +0x8b
git.sr.ht/~rjarry/aerc/config.(*UIConfig).GetStyle(...)
	config/ui.go:379
git.sr.ht/~rjarry/aerc/lib/ui.(*TabStrip).Draw(0xc000314700, 0xc000192230)
	lib/ui/tab.go:378 +0x15b
git.sr.ht/~rjarry/aerc/lib/ui.(*Grid).Draw(0xc000186fc0, 0xc0002c25f0)
	lib/ui/grid.go:126 +0x28e
git.sr.ht/~rjarry/aerc/app.(*Aerc).Draw(0x14b9f00, 0xc0002c25f0)
	app/aerc.go:192 +0x1fe
git.sr.ht/~rjarry/aerc/lib/ui.Render()
	lib/ui/ui.go:155 +0x16b
main.main()
	main.go:310 +0x997

Make the reload operation safe by changing how config objects are
exposed and updated. Change all objects to be atomic pointers. Expose
public functions to access their value atomically. Only update their
value after a complete and successful config parse. This way the UI
thread will always have access to a valid configuration.

NB: The account configuration is not included in this change since it
cannot be reloaded.

Fixes: https://todo.sr.ht/~rjarry/aerc/319
Reported-by: Anachron <gith@cron.world>
Signed-off-by: Robin Jarry <robin@jarry.cc>
2025-09-23 14:02:37 +02:00

988 lines
24 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"os/exec"
"sort"
"strings"
"time"
"unicode"
"git.sr.ht/~rjarry/go-opt/v2"
"git.sr.ht/~rockorager/vaxis"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/emersion/go-message/mail"
"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/log"
"git.sr.ht/~rjarry/aerc/lib/ui"
"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(ctx context.Context, cmd string) ([]opt.Completion, 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
beep func()
dialog ui.DrawableInteractive
Crypto crypto.Provider
}
type Choice struct {
Key string
Text string
Command string
}
func (aerc *Aerc) Init(
crypto crypto.Provider,
cmd func(string, *config.AccountConfig, *models.MessageInfo) error,
complete func(ctx context.Context, cmd string) ([]opt.Completion, string), cmdHistory lib.History,
deferLoop chan struct{},
) {
tabs := ui.NewTabs(func(d ui.Drawable) *config.UIConfig {
acct := aerc.account(d)
if acct != nil {
return config.Ui().ForAccount(acct.Name())
}
return 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.accounts = make(map[string]*AccountView)
aerc.cmd = cmd
aerc.cmdHistory = cmdHistory
aerc.complete = complete
aerc.grid = grid
aerc.statusbar = statusbar
aerc.statusline = statusline
aerc.prompts = ui.NewStack(config.Ui())
aerc.tabs = tabs
aerc.Crypto = crypto
for _, acct := range config.Accounts {
view, err := NewAccountView(acct, deferLoop)
if err != nil {
tabs.Add(errorScreen(err.Error()), acct.Name, false)
} else {
aerc.accounts[acct.Name] = view
view.tab = tabs.Add(view, acct.Name, false)
}
}
if len(config.Accounts) == 0 {
wizard := NewAccountWizard()
wizard.Focus(true)
aerc.NewTab(wizard, "New account", false)
}
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()
}
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()) {
aerc.beep = f
}
func (aerc *Aerc) Beep() {
if aerc.beep == nil {
log.Warnf("should beep, but no beeper")
return
}
aerc.beep()
}
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 {
w, h := ctx.Width(), ctx.Height()
if d, ok := aerc.dialog.(Dialog); ok {
xstart, width := d.ContextWidth()
ystart, height := d.ContextHeight()
aerc.dialog.Draw(
ctx.Subcontext(xstart(w), ystart(h),
width(w), height(h)))
} else if w > 8 && h > 4 {
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, "%", "%%")
}
annotate := func(b *config.Binding) string {
if b.Annotation == "" {
return ""
}
return "[" + b.Annotation + "]"
}
fmtStr := "%10s %s %s"
for _, bind := range binds.Bindings {
result = append(result, fmt.Sprintf(fmtStr,
format(config.FormatKeyStrokes(bind.Input)),
format(config.FormatKeyStrokes(bind.Output)),
annotate(bind),
))
}
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)),
annotate(bind),
))
}
}
result = append(result, fmt.Sprintf(fmtStr,
"$ex",
fmt.Sprintf("'%c'", binds.ExKey.Key), "",
))
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:
var binds *config.KeyBindings
switch view.Bindings() {
case "compose::editor":
binds = config.Binds().ComposeEditor.ForAccount(
selectedAccountName)
case "compose::review":
binds = config.Binds().ComposeReview.ForAccount(
selectedAccountName)
default:
binds = config.Binds().Compose.ForAccount(
selectedAccountName)
}
return binds.ForFolder(view.SelectedDirectory())
case *MessageViewer:
var binds *config.KeyBindings
switch view.Bindings() {
case "view::passthrough":
binds = config.Binds().MessageViewPassthrough.ForAccount(
selectedAccountName)
default:
binds = config.Binds().MessageView.ForAccount(
selectedAccountName)
}
return binds.ForFolder(view.SelectedAccount().SelectedDirectory())
case *Terminal:
return config.Binds().Terminal
default:
return config.Binds().Global
}
}
func (aerc *Aerc) simulate(strokes []config.KeyStroke) {
aerc.pendingKeys = []config.KeyStroke{}
bindings := aerc.getBindings()
complete := aerc.SelectedAccountUiConfig().CompletionMinChars != config.MANUAL_COMPLETE
aerc.simulating += 1
for _, stroke := range strokes {
simulated := vaxis.Key{
Keycode: stroke.Key,
Modifiers: stroke.Modifiers,
}
if unicode.IsUpper(stroke.Key) {
simulated.Keycode = unicode.ToLower(stroke.Key)
simulated.Modifiers |= vaxis.ModShift
}
// If none of these mods are present, set the text field to
// enable matching keys like ":"
if stroke.Modifiers&vaxis.ModCtrl == 0 &&
stroke.Modifiers&vaxis.ModAlt == 0 &&
stroke.Modifiers&vaxis.ModSuper == 0 &&
stroke.Modifiers&vaxis.ModHyper == 0 {
simulated.Text = string(stroke.Key)
}
aerc.Event(simulated)
complete = stroke == bindings.CompleteKey
}
aerc.simulating -= 1
if exline, ok := aerc.focused.(*ExLine); ok {
// we are still focused on the exline, turn on tab complete
exline.TabComplete(func(ctx context.Context, cmd string) ([]opt.Completion, string) {
return aerc.complete(ctx, cmd)
})
if complete {
// force completion now
exline.Event(vaxis.Key{Keycode: vaxis.KeyTab})
}
}
}
func (aerc *Aerc) Event(event vaxis.Event) bool {
if config.General().QuakeMode {
if e, ok := event.(vaxis.Key); ok && e.MatchString("F1") {
ToggleQuake()
return true
}
}
if aerc.dialog != nil {
return aerc.dialog.Event(event)
}
if aerc.focused != nil {
return aerc.focused.Event(event)
}
switch event := event.(type) {
// TODO: more vaxis events handling
case vaxis.Key:
// 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()
stroke := config.KeyStroke{
Modifiers: event.Modifiers,
}
switch {
case event.ShiftedCode != 0:
stroke.Key = event.ShiftedCode
stroke.Modifiers &^= vaxis.ModShift
default:
stroke.Key = event.Keycode
}
aerc.pendingKeys = append(aerc.pendingKeys, stroke)
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 vaxis.Mouse:
aerc.grid.MouseEvent(event.Col, event.Row, event)
return true
case vaxis.PasteStartEvent:
aerc.pasting = true
interactive, ok := aerc.SelectedTabContent().(ui.Interactive)
if ok {
return interactive.Event(event)
}
return false
case vaxis.PasteEndEvent:
aerc.pasting = false
interactive, ok := aerc.SelectedTabContent().(ui.Interactive)
if ok {
return interactive.Event(event)
}
return false
case vaxis.FocusIn:
if focusable, ok := aerc.SelectedTabContent().(ui.Focusable); ok {
focusable.Focus(true)
return true
}
return false
case vaxis.FocusOut:
if focusable, ok := aerc.SelectedTabContent().(ui.Focusable); ok {
focusable.Focus(false)
return true
}
return false
case vaxis.ColorThemeUpdate:
// Color theme changes need to be sent to all the tabs because
// there may be for instance multiple open virtual terminals
// each running a program that needs to be made aware of the
// change in order to repaint itself.
processed := false
i := 0
for tab := aerc.tabs.Get(0); tab != nil; tab = aerc.tabs.Get(i) {
i++
if interactive, ok := tab.Content.(ui.Interactive); ok {
processed = interactive.Event(event) || processed
}
}
return processed
}
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, background bool) *ui.Tab {
tab := aerc.tabs.Add(clickable, name, background)
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) SelectTabAtOffset(offset int) {
aerc.tabs.SelectOffset(offset)
}
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(context.Context, string) ([]opt.Completion, string)
if aerc.simulating != 0 {
// Don't try to draw completions for simulated events
tabComplete = nil
} else {
tabComplete = aerc.complete
}
exline := NewExLine(cmd, func(cmd string) {
err := aerc.cmd(cmd, 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 += " " + opt.QuoteArg(text)
}
err := aerc.cmd(cmd, nil, nil)
if err != nil {
aerc.PushError(err.Error())
}
}, func(ctx context.Context, cmd string) ([]opt.Completion, 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(ctx context.Context, cmd string) ([]opt.Completion, string) {
return nil, "" // TODO: completions
})
aerc.prompts.Push(p)
}
func (aerc *Aerc) Command(args []string) error {
switch {
case len(args) == 0:
return nil // noop success, i.e. ping
case strings.HasPrefix(args[0], "mailto:"):
mailto, err := url.Parse(args[0])
if err != nil {
return err
}
return aerc.mailto(mailto)
case strings.HasPrefix(args[0], "mbox:"):
return aerc.mbox(args[0])
case strings.HasPrefix(args[0], ":"):
cmdline := args[0]
if len(args) > 1 {
cmdline = opt.QuoteArgs(args...).String()
}
defer ui.Invalidate()
return aerc.cmd(cmdline, nil, nil)
default:
return errors.New("command not understood")
}
}
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(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, false)
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 = []string{"Sent"}
defer ui.Invalidate()
mboxView, err := NewAccountView(&acctConf, nil)
if err != nil {
aerc.NewTab(errorScreen(err.Error()), acctConf.Name, false)
} else {
aerc.accounts[acctConf.Name] = mboxView
aerc.NewTab(mboxView, acctConf.Name, false)
}
return nil
}
func (aerc *Aerc) CloseBackends() error {
var returnErr error
for _, acct := range aerc.accounts {
var raw any = 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) 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(' ', vaxis.Style{})).At(0, 0)
grid.AddChild(text).At(1, 0)
grid.AddChild(ui.NewFill(' ', vaxis.Style{})).At(2, 0)
return grid
}
func (aerc *Aerc) isExKey(key vaxis.Key, exKey config.KeyStroke) bool {
return key.Matches(exKey.Key, exKey.Modifiers)
}
// CmdFallbackSearch checks cmds for the first executable available in PATH. An error is
// returned if none are found
func CmdFallbackSearch(cmds []string, silent bool) (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)
if !silent {
warn := fmt.Sprintf("cmd '%s' not found in PATH, using fallback", cmd)
PushWarning(warn)
}
continue
}
return cmd, nil
}
return "", fmt.Errorf("no command found in PATH: %s", tried)
}