diff -Nru aerc-0.13.0/aerc.go aerc-0.14.0/aerc.go --- aerc-0.13.0/aerc.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/aerc.go 2023-01-04 15:38:38.000000000 +0000 @@ -26,7 +26,7 @@ "git.sr.ht/~rjarry/aerc/lib/crypto" "git.sr.ht/~rjarry/aerc/lib/templates" libui "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" ) @@ -113,19 +113,19 @@ } func setWindowTitle() { - logging.Debugf("Parsing terminfo") + log.Tracef("Parsing terminfo") ti, err := terminfo.LoadFromEnv() if err != nil { - logging.Warnf("Cannot get terminfo: %v", err) + log.Warnf("Cannot get terminfo: %v", err) return } if !ti.Has(terminfo.HasStatusLine) { - logging.Infof("Terminal does not have status line support") + log.Infof("Terminal does not have status line support") return } - logging.Infof("Setting terminal title") + log.Debugf("Setting terminal title") buf := new(bytes.Buffer) ti.Fprintf(buf, terminfo.ToStatusLine) fmt.Fprint(buf, "aerc") @@ -134,17 +134,17 @@ } func main() { - defer logging.PanicHandler() + defer log.PanicHandler() opts, optind, err := getopt.Getopts(os.Args, "va:") if err != nil { usage("error: " + err.Error()) return } - logging.BuildInfo = buildInfo() + log.BuildInfo = buildInfo() var accts []string for _, opt := range opts { if opt.Option == 'v' { - fmt.Println("aerc " + logging.BuildInfo) + fmt.Println("aerc " + log.BuildInfo) return } if opt.Option == 'a' { @@ -167,17 +167,14 @@ retryExec = true } - if !isatty.IsTerminal(os.Stdout.Fd()) { - logging.Init() - } - logging.Infof("Starting up version %s", logging.BuildInfo) - - conf, err := config.LoadConfigFromFile(nil, accts) + err = config.LoadConfigFromFile(nil, accts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err) os.Exit(1) //nolint:gocritic // PanicHandler does not need to run as it's not a panic } + log.Infof("Starting up version %s", log.BuildInfo) + var ( aerc *widgets.Aerc ui *libui.UI @@ -185,14 +182,14 @@ deferLoop := make(chan struct{}) - c := crypto.New(conf.General.PgpProvider) + c := crypto.New() err = c.Init() if err != nil { - logging.Warnf("failed to initialise crypto interface: %v", err) + log.Warnf("failed to initialise crypto interface: %v", err) } defer c.Close() - aerc = widgets.NewAerc(conf, c, func(cmd []string) error { + aerc = widgets.NewAerc(c, func(cmd []string) error { return execCommand(aerc, ui, cmd) }, func(cmd string) []string { return getCompletions(aerc, cmd) @@ -203,18 +200,18 @@ panic(err) } defer ui.Close() - logging.UICleanup = func() { + log.UICleanup = func() { ui.Close() } close(deferLoop) - if conf.Ui.MouseEnabled { + if config.Ui.MouseEnabled { ui.EnableMouse() } as, err := lib.StartServer() if err != nil { - logging.Warnf("Failed to start Unix server: %v", err) + log.Warnf("Failed to start Unix server: %v", err) } else { defer as.Close() as.OnMailto = aerc.Mailto @@ -232,7 +229,7 @@ fmt.Fprintf(os.Stderr, "Failed to communicate to aerc: %v\n", err) err = aerc.CloseBackends() if err != nil { - logging.Warnf("failed to close backends: %v", err) + log.Warnf("failed to close backends: %v", err) } return } @@ -259,6 +256,6 @@ } err = aerc.CloseBackends() if err != nil { - logging.Warnf("failed to close backends: %v", err) + log.Warnf("failed to close backends: %v", err) } } diff -Nru aerc-0.13.0/.builds/alpine-edge.yml aerc-0.14.0/.builds/alpine-edge.yml --- aerc-0.13.0/.builds/alpine-edge.yml 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/.builds/alpine-edge.yml 2023-01-04 15:38:38.000000000 +0000 @@ -30,3 +30,6 @@ cd aerc make clean make + - check-patches: | + cd aerc + make check-patches diff -Nru aerc-0.13.0/CHANGELOG.md aerc-0.14.0/CHANGELOG.md --- aerc-0.13.0/CHANGELOG.md 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/CHANGELOG.md 2023-01-04 15:38:38.000000000 +0000 @@ -5,6 +5,54 @@ ## [Unreleased](https://git.sr.ht/~rjarry/aerc/log/master) +## [0.14.0](https://git.sr.ht/~rjarry/aerc/refs/0.14.0) - 2023-01-04 + +### Added + +- View common email envelope headers with `:envelope`. +- Notmuch accounts now support maildir operations: `:copy`, `:move`, `:mkdir`, + `:rmdir`, `:archive` and the `copy-to` option. +- Display messages from bottom to top with `[ui].reverse-msglist-order=true` in + `aerc.conf`. +- Display threads from bottom to top with `[ui].reverse-thread-order=true` in + `aerc.conf`. +- Style search results in the message list with `msglist_result.*`. +- Preview messages with their attachments before sending with `:preview`. +- Filter commands now have `AERC_FORMAT`, `AERC_SUBJECT` and `AERC_FROM` + defined in their environment. +- Override the subject prefix for replies pattern with `subject-re-pattern` in + `accounts.conf`. +- Search/filter by absolute and relative date ranges with the `-d` flag. +- LIST-STATUS and ORDEREDSUBJECT threading extensions support for imap. +- Built-in `wrap` filter that does not mess up nested quotes and lists. +- Write `multipart/alternative` messages with `:multipart` and commands defined + in the new `[multipart-converters]` section of `aerc.conf`. +- Close the message viewer before opening the composer with `:reply -c`. +- Attachment indicator in message list flags (by default `a`, but can be + changed via `[ui].icon-attachment` in `aerc.conf`). +- Open file picker menu with `:attach -m`. The menu must be generated by an + external command configured via `[compose].file-picker-cmd` in `aerc.conf`. +- Sample stylesets are now installed in `$PREFIX/share/aerc/stylesets`. +- The built-in `colorize` filter now has different themes. + +### Changed + +- `pgp-provider` now defaults to `auto`. It will use the system `gpg` unless + the internal keyring exists and contains at least one key. +- Calling `:split` or `:vsplit` without specifying a size, now attempts to use + the terminal size to determine a useful split-size. + +### Fixed + +- `:pipe -m git am -3` on patch series when `Message-Id` headers have not been + generated by `git send-email`. +- Overflowing text in header editors while composing can now be scrolled + horizontally. + +### Deprecated + +- Removed broken `:set` command. + ## [0.13.0](https://git.sr.ht/~rjarry/aerc/refs/0.13.0) - 2022-10-20 ### Added diff -Nru aerc-0.13.0/commands/account/compose.go aerc-0.14.0/commands/account/compose.go --- aerc-0.13.0/commands/account/compose.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/compose.go 2023-01-04 15:38:38.000000000 +0000 @@ -10,8 +10,9 @@ "github.com/emersion/go-message/mail" + "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~sircmpwn/getopt" @@ -41,7 +42,7 @@ return errors.New("No account selected") } if template == "" { - template = aerc.Config().Templates.NewMessage + template = config.Templates.NewMessage } msg, err := gomail.ReadMessage(strings.NewReader(body)) @@ -53,7 +54,7 @@ headers := mail.HeaderFromMap(msg.Header) composer, err := widgets.NewComposer(aerc, acct, - aerc.Config(), acct.AccountConfig(), acct.Worker(), + acct.AccountConfig(), acct.Worker(), template, &headers, models.OriginalMail{}) if err != nil { return err @@ -68,7 +69,7 @@ ui.Invalidate() }) go func() { - defer logging.PanicHandler() + defer log.PanicHandler() composer.AppendContents(msg.Body) }() diff -Nru aerc-0.13.0/commands/account/export-mbox.go aerc-0.14.0/commands/account/export-mbox.go --- aerc-0.13.0/commands/account/export-mbox.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/export-mbox.go 2023-01-04 15:38:38.000000000 +0000 @@ -8,7 +8,7 @@ "sync" "time" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" mboxer "git.sr.ht/~rjarry/aerc/worker/mbox" "git.sr.ht/~rjarry/aerc/worker/types" @@ -55,7 +55,7 @@ go func() { file, err := os.Create(filename) if err != nil { - logging.Errorf("failed to create file: %v", err) + log.Errorf("failed to create file: %v", err) aerc.PushError(err.Error()) return } @@ -74,16 +74,16 @@ if retries > 0 { if retries > 10 { errorMsg := fmt.Sprintf("too many retries: %d; stopping export", retries) - logging.Errorf(errorMsg) + log.Errorf(errorMsg) aerc.PushError(args[0] + " " + errorMsg) break } sleeping := time.Duration(retries * 1e9 * 2) - logging.Debugf("sleeping for %s before retrying; retries: %d", sleeping, retries) + log.Debugf("sleeping for %s before retrying; retries: %d", sleeping, retries) time.Sleep(sleeping) } - logging.Debugf("fetching %d for export", len(uids)) + log.Debugf("fetching %d for export", len(uids)) acct.Worker().PostAction(&types.FetchFullMessages{ Uids: uids, }, func(msg types.WorkerMessage) { @@ -91,14 +91,14 @@ case *types.Done: done <- true case *types.Error: - logging.Errorf("failed to fetch message: %v", msg.Error) + log.Errorf("failed to fetch message: %v", msg.Error) aerc.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 { - logging.Warnf("failed to write mbox: %v", err) + log.Warnf("failed to write mbox: %v", err) } for i, uid := range uids { if uid == msg.Content.Uid { @@ -117,7 +117,7 @@ } statusInfo := fmt.Sprintf("Exported %d of %d messages to %s.", ctr, len(store.Uids()), filename) aerc.PushStatus(statusInfo, 10*time.Second) - logging.Infof(statusInfo) + log.Debugf(statusInfo) }() return nil diff -Nru aerc-0.13.0/commands/account/import-mbox.go aerc-0.14.0/commands/account/import-mbox.go --- aerc-0.13.0/commands/account/import-mbox.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/import-mbox.go 2023-01-04 15:38:38.000000000 +0000 @@ -11,7 +11,7 @@ "time" "git.sr.ht/~rjarry/aerc/commands" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" mboxer "git.sr.ht/~rjarry/aerc/worker/mbox" @@ -55,7 +55,7 @@ importFolder := func() { statusInfo := fmt.Sprintln("Importing", filename, "to folder", folder) aerc.PushStatus(statusInfo, 10*time.Second) - logging.Infof(statusInfo) + log.Debugf(statusInfo) f, err := os.Open(filename) if err != nil { aerc.PushError(err.Error()) @@ -78,7 +78,7 @@ var buf bytes.Buffer r, err := m.NewReader() if err != nil { - logging.Errorf("could not get reader for uid %d", m.UID()) + log.Errorf("could not get reader for uid %d", m.UID()) break } nbytes, _ := io.Copy(&buf, r) @@ -92,11 +92,11 @@ switch msg := msg.(type) { case *types.Unsupported: errMsg := fmt.Sprintf("%s: AppendMessage is unsupported", args[0]) - logging.Errorf(errMsg) + log.Errorf(errMsg) aerc.PushError(errMsg) return case *types.Error: - logging.Errorf("AppendMessage failed: %v", msg.Error) + log.Errorf("AppendMessage failed: %v", msg.Error) done <- false case *types.Done: atomic.AddUint32(&appended, 1) @@ -113,17 +113,17 @@ retries -= 1 sleeping := time.Duration((5 - retries) * 1e9) - logging.Debugf("sleeping for %s before append message %d again", sleeping, i) + log.Debugf("sleeping for %s before append message %d again", sleeping, i) time.Sleep(sleeping) } case <-time.After(30 * time.Second): - logging.Warnf("timed-out; appended %d of %d", appended, len(messages)) + log.Warnf("timed-out; appended %d of %d", appended, len(messages)) return } } } infoStr := fmt.Sprintf("%s: imported %d of %d sucessfully.", args[0], appended, len(messages)) - logging.Infof(infoStr) + log.Debugf(infoStr) aerc.SetStatus(infoStr) } diff -Nru aerc-0.13.0/commands/account/recover.go aerc-0.14.0/commands/account/recover.go --- aerc-0.13.0/commands/account/recover.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/recover.go 2023-01-04 15:38:38.000000000 +0000 @@ -9,7 +9,7 @@ "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~sircmpwn/getopt" @@ -102,7 +102,7 @@ } composer, err := widgets.NewComposer(aerc, acct, - aerc.Config(), acct.AccountConfig(), acct.Worker(), + acct.AccountConfig(), acct.Worker(), "", nil, models.OriginalMail{}) if err != nil { return err @@ -114,7 +114,7 @@ ui.Invalidate() }) go func() { - defer logging.PanicHandler() + defer log.PanicHandler() composer.AppendContents(bytes.NewReader(data)) }() diff -Nru aerc-0.13.0/commands/account/search.go aerc-0.14.0/commands/account/search.go --- aerc-0.13.0/commands/account/search.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/search.go 2023-01-04 15:38:38.000000000 +0000 @@ -6,7 +6,7 @@ "git.sr.ht/~rjarry/aerc/lib/statusline" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" ) @@ -44,7 +44,7 @@ cb := func(msg types.WorkerMessage) { if _, ok := msg.(*types.Done); ok { acct.SetStatus(statusline.FilterResult(strings.Join(args, " "))) - logging.Infof("Filter results: %v", store.Uids()) + log.Tracef("Filter results: %v", store.Uids()) } } store.Sort(store.GetCurrentSortCriteria(), cb) @@ -52,7 +52,7 @@ acct.SetStatus(statusline.Search("Searching...")) cb := func(uids []uint32) { acct.SetStatus(statusline.Search(strings.Join(args, " "))) - logging.Infof("Search results: %v", uids) + log.Tracef("Search results: %v", uids) store.ApplySearch(uids) // TODO: Remove when stores have multiple OnUpdate handlers ui.Invalidate() diff -Nru aerc-0.13.0/commands/account/split.go aerc-0.14.0/commands/account/split.go --- aerc-0.13.0/commands/account/split.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/split.go 2023-01-04 15:38:38.000000000 +0000 @@ -30,10 +30,15 @@ if acct == nil { return errors.New("No account selected") } - if acct.Messages().Empty() { - return nil - } n := 0 + if acct.SplitSize() == 0 { + if args[0] == "split" { + n = aerc.SelectedAccount().Messages().Height() / 4 + } else { + n = aerc.SelectedAccount().Messages().Width() / 2 + } + } + var err error if len(args) > 1 { delta := false @@ -46,10 +51,8 @@ } if delta { n = acct.SplitSize() + n - // Maintain split direction when using deltas - if acct.SplitSize() > 0 { - args[0] = acct.SplitDirection() - } + acct.SetSplitSize(n) + return nil } } if n == acct.SplitSize() { @@ -61,8 +64,11 @@ // Don't allow split to go negative n = 1 } - if args[0] == "split" { + switch args[0] { + case "split": return acct.Split(n) + case "vsplit": + return acct.Vsplit(n) } - return acct.Vsplit(n) + return nil } diff -Nru aerc-0.13.0/commands/account/view.go aerc-0.14.0/commands/account/view.go --- aerc-0.13.0/commands/account/view.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/account/view.go 2023-01-04 15:38:38.000000000 +0000 @@ -65,7 +65,7 @@ aerc.PushError(err.Error()) return } - viewer := widgets.NewMessageViewer(acct, aerc.Config(), view) + viewer := widgets.NewMessageViewer(acct, view) aerc.NewTab(viewer, msg.Envelope.Subject) }) return nil diff -Nru aerc-0.13.0/commands/compose/attach.go aerc-0.14.0/commands/compose/attach.go --- aerc-0.13.0/commands/compose/attach.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/compose/attach.go 2023-01-04 15:38:38.000000000 +0000 @@ -1,14 +1,19 @@ package compose import ( + "bufio" "errors" "fmt" + "io" "os" + "os/exec" "path/filepath" "strings" "git.sr.ht/~rjarry/aerc/commands" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/config" + "git.sr.ht/~rjarry/aerc/lib/ui" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" "github.com/mitchellh/go-homedir" ) @@ -28,36 +33,52 @@ return commands.CompletePath(path) } -func (Attach) Execute(aerc *widgets.Aerc, args []string) error { +func (a Attach) Execute(aerc *widgets.Aerc, args []string) error { if len(args) == 1 { return fmt.Errorf("Usage: :attach ") } - path := strings.Join(args[1:], " ") + if args[1] == "-m" { + return a.openMenu(aerc, args[2:]) + } + + return a.addPath(aerc, strings.Join(args[1:], " ")) +} + +func (a Attach) addPath(aerc *widgets.Aerc, path string) error { path, err := homedir.Expand(path) if err != nil { - logging.Errorf("failed to expand path '%s': %v", path, err) + log.Errorf("failed to expand path '%s': %v", path, err) aerc.PushError(err.Error()) return err } - logging.Debugf("attaching %s", path) - attachments, err := filepath.Glob(path) if err != nil && errors.Is(err, filepath.ErrBadPattern) { - logging.Warnf("failed to parse as globbing pattern: %v", err) + log.Warnf("failed to parse as globbing pattern: %v", err) attachments = []string{path} } - logging.Debugf("filenames: %v", attachments) + if !strings.HasPrefix(path, ".") && !strings.Contains(path, "/.") { + log.Debugf("removing hidden files from glob results") + for i := len(attachments) - 1; i >= 0; i-- { + if strings.HasPrefix(filepath.Base(attachments[i]), ".") { + if i == len(attachments)-1 { + attachments = attachments[:i] + continue + } + attachments = append(attachments[:i], attachments[i+1:]...) + } + } + } composer, _ := aerc.SelectedTabContent().(*widgets.Composer) for _, attach := range attachments { - logging.Debugf("attaching '%s'", attach) + log.Debugf("attaching '%s'", attach) pathinfo, err := os.Stat(attach) if err != nil { - logging.Errorf("failed to stat file: %v", err) + log.Errorf("failed to stat file: %v", err) aerc.PushError(err.Error()) return err } else if pathinfo.IsDir() && len(attachments) == 1 { @@ -76,3 +97,82 @@ return nil } + +func (a Attach) openMenu(aerc *widgets.Aerc, args []string) error { + filePickerCmd := config.Compose.FilePickerCmd + if filePickerCmd == "" { + return fmt.Errorf("no file-picker-cmd defined") + } + + if strings.Contains(filePickerCmd, "%s") { + verb := "" + if len(args) > 0 { + verb = args[0] + } + filePickerCmd = strings.ReplaceAll(filePickerCmd, "%s", verb) + } + + picks, err := os.CreateTemp("", "aerc-filepicker-*") + if err != nil { + return err + } + + filepicker := exec.Command("sh", "-c", filePickerCmd+" >&3") + filepicker.ExtraFiles = append(filepicker.ExtraFiles, picks) + + t, err := widgets.NewTerminal(filepicker) + if err != nil { + return err + } + t.OnClose = func(err error) { + defer func() { + if err := picks.Close(); err != nil { + log.Errorf("error closing file: %v", err) + } + if err := os.Remove(picks.Name()); err != nil { + log.Errorf("could not remove tmp file: %v", err) + } + }() + + aerc.CloseDialog() + + if err != nil { + log.Errorf("terminal closed with error: %v", err) + return + } + + _, err = picks.Seek(0, io.SeekStart) + if err != nil { + log.Errorf("seek failed: %v", err) + return + } + + scanner := bufio.NewScanner(picks) + for scanner.Scan() { + f := strings.TrimSpace(scanner.Text()) + if _, err := os.Stat(f); err != nil { + continue + } + log.Tracef("File picker attaches: %v", f) + err := a.addPath(aerc, f) + if err != nil { + log.Errorf("attach failed for file %s: %v", f, err) + } + + } + } + + aerc.AddDialog(widgets.NewDialog( + ui.NewBox(t, "File Picker", "", aerc.SelectedAccountUiConfig()), + // start pos on screen + func(h int) int { + return h / 8 + }, + // dialog height + func(h int) int { + return h - 2*h/8 + }, + )) + + return nil +} diff -Nru aerc-0.13.0/commands/compose/multipart.go aerc-0.14.0/commands/compose/multipart.go --- aerc-0.13.0/commands/compose/multipart.go 1970-01-01 00:00:00.000000000 +0000 +++ aerc-0.14.0/commands/compose/multipart.go 2023-01-04 15:38:38.000000000 +0000 @@ -0,0 +1,74 @@ +package compose + +import ( + "bytes" + "fmt" + + "git.sr.ht/~rjarry/aerc/commands" + "git.sr.ht/~rjarry/aerc/config" + "git.sr.ht/~rjarry/aerc/widgets" + "git.sr.ht/~sircmpwn/getopt" +) + +type Multipart struct{} + +func init() { + register(Multipart{}) +} + +func (Multipart) Aliases() []string { + return []string{"multipart"} +} + +func (Multipart) Complete(aerc *widgets.Aerc, args []string) []string { + var completions []string + completions = append(completions, "-d") + for mime := range config.Converters { + completions = append(completions, mime) + } + return commands.CompletionFromList(aerc, completions, args) +} + +func (a Multipart) Execute(aerc *widgets.Aerc, args []string) error { + composer, ok := aerc.SelectedTabContent().(*widgets.Composer) + if !ok { + return fmt.Errorf(":multipart is only available on the compose::review screen") + } + + opts, optind, err := getopt.Getopts(args, "d") + if err != nil { + return fmt.Errorf("Usage: :multipart [-d] ") + } + var remove bool = false + for _, opt := range opts { + if opt.Option == 'd' { + remove = true + } + } + args = args[optind:] + if len(args) != 1 { + return fmt.Errorf("Usage: :multipart [-d] ") + } + mime := args[0] + + if remove { + return composer.RemovePart(mime) + } else { + _, found := config.Converters[mime] + if !found { + return fmt.Errorf("no command defined for MIME type: %s", mime) + } + err = composer.AppendPart( + mime, + map[string]string{"Charset": "UTF-8"}, + // the actual content of the part will be rendered + // every time the body of the email is updated + bytes.NewReader([]byte{}), + ) + if err != nil { + return err + } + } + + return nil +} diff -Nru aerc-0.13.0/commands/compose/postpone.go aerc-0.14.0/commands/compose/postpone.go --- aerc-0.13.0/commands/compose/postpone.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/compose/postpone.go 2023-01-04 15:38:38.000000000 +0000 @@ -7,7 +7,7 @@ "github.com/miolini/datacounter" "github.com/pkg/errors" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" @@ -47,7 +47,7 @@ return errors.New("No Postpone location configured") } - logging.Infof("Postponing mail") + log.Tracef("Postponing mail") header, err := composer.PrepareHeader() if err != nil { @@ -70,7 +70,7 @@ // run this as a goroutine so we can make other progress. The message // will be saved once the directory is created. go func() { - defer logging.PanicHandler() + defer log.PanicHandler() errStr := <-errChan if errStr != "" { @@ -80,7 +80,7 @@ handleErr := func(err error) { aerc.PushError(err.Error()) - logging.Errorf("Postponing failed: %v", err) + log.Errorf("Postponing failed: %v", err) aerc.NewTab(composer, tabName) } diff -Nru aerc-0.13.0/commands/compose/send.go aerc-0.14.0/commands/compose/send.go --- aerc-0.13.0/commands/compose/send.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/compose/send.go 2023-01-04 15:38:38.000000000 +0000 @@ -17,7 +17,7 @@ "git.sr.ht/~rjarry/aerc/commands/mode" "git.sr.ht/~rjarry/aerc/lib" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" @@ -104,11 +104,11 @@ if err != nil || warn { msg := "You may have forgotten an attachment." if err != nil { - logging.Warnf("failed to check for a forgotten attachment: %v", err) + log.Warnf("failed to check for a forgotten attachment: %v", err) msg = "Failed to check for a forgotten attachment." } - prompt := widgets.NewPrompt(aerc.Config(), + prompt := widgets.NewPrompt( msg+" Abort send? [Y/n] ", func(text string) { if text == "n" || text == "N" { @@ -148,7 +148,7 @@ failCh := make(chan error) // writer go func() { - defer logging.PanicHandler() + defer log.PanicHandler() var sender io.WriteCloser var err error @@ -182,7 +182,7 @@ // cleanup + copy to sent go func() { - defer logging.PanicHandler() + defer log.PanicHandler() // leave no-quit mode defer mode.NoQuitDone() @@ -325,14 +325,13 @@ OAuth2: oauth2, Enabled: true, } - if bearer.OAuth2.Endpoint.TokenURL == "" { - return nil, fmt.Errorf("No 'TokenURL' configured for this account") - } - token, err := bearer.ExchangeRefreshToken(password) - if err != nil { - return nil, err + if bearer.OAuth2.Endpoint.TokenURL != "" { + token, err := bearer.ExchangeRefreshToken(password) + if err != nil { + return nil, err + } + password = token.AccessToken } - password = token.AccessToken saslClient = sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{ Username: uri.User.Username(), Token: password, diff -Nru aerc-0.13.0/commands/eml.go aerc-0.14.0/commands/eml.go --- aerc-0.13.0/commands/eml.go 1970-01-01 00:00:00.000000000 +0000 +++ aerc-0.14.0/commands/eml.go 2023-01-04 15:38:38.000000000 +0000 @@ -0,0 +1,83 @@ +package commands + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + + "git.sr.ht/~rjarry/aerc/lib" + "git.sr.ht/~rjarry/aerc/widgets" +) + +type Eml struct{} + +func init() { + register(Eml{}) +} + +func (Eml) Aliases() []string { + return []string{"eml", "preview"} +} + +func (Eml) Complete(aerc *widgets.Aerc, args []string) []string { + return CompletePath(strings.Join(args, " ")) +} + +func (Eml) Execute(aerc *widgets.Aerc, args []string) error { + acct := aerc.SelectedAccount() + if acct == nil { + return fmt.Errorf("no account selected") + } + + showEml := func(r io.Reader) { + data, err := io.ReadAll(r) + if err != nil { + aerc.PushError(err.Error()) + return + } + lib.NewEmlMessageView(data, aerc.Crypto, aerc.DecryptKeys, + func(view lib.MessageView, err error) { + if err != nil { + aerc.PushError(err.Error()) + return + } + msgView := widgets.NewMessageViewer(acct, view) + aerc.NewTab(msgView, + view.MessageInfo().Envelope.Subject) + }) + } + + if len(args) == 1 { + switch tab := aerc.SelectedTabContent().(type) { + case *widgets.MessageViewer: + part := tab.SelectedMessagePart() + tab.MessageView().FetchBodyPart(part.Index, showEml) + case *widgets.Composer: + var buf bytes.Buffer + h, err := tab.PrepareHeader() + if err != nil { + return err + } + if err := tab.WriteMessage(h, &buf); err != nil { + return err + } + showEml(&buf) + default: + return fmt.Errorf("unsupported operation") + } + } else { + path := strings.Join(args[1:], " ") + if _, err := os.Stat(path); err != nil { + return err + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + showEml(f) + } + return nil +} diff -Nru aerc-0.13.0/commands/exec.go aerc-0.14.0/commands/exec.go --- aerc-0.13.0/commands/exec.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/exec.go 2023-01-04 15:38:38.000000000 +0000 @@ -7,7 +7,7 @@ "os/exec" "time" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" ) @@ -46,7 +46,7 @@ cmd.Env = env go func() { - defer logging.PanicHandler() + defer log.PanicHandler() err := cmd.Run() if err != nil { diff -Nru aerc-0.13.0/commands/help.go aerc-0.14.0/commands/help.go --- aerc-0.13.0/commands/help.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/help.go 2023-01-04 15:38:38.000000000 +0000 @@ -10,6 +10,8 @@ var pages = []string{ "aerc", + "accounts", + "binds", "config", "imap", "notmuch", diff -Nru aerc-0.13.0/commands/history.go aerc-0.14.0/commands/history.go --- aerc-0.13.0/commands/history.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/history.go 2023-01-04 15:38:38.000000000 +0000 @@ -9,7 +9,7 @@ "path" "sync" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "github.com/kyoh86/xdg" ) @@ -105,7 +105,7 @@ 0o600, ) if err != nil { - logging.Errorf("failed to open history file: %v", err) + log.Errorf("failed to open history file: %v", err) // basically mirror the old behavior h.histfile = bytes.NewBuffer([]byte{}) return diff -Nru aerc-0.13.0/commands/msg/archive.go aerc-0.14.0/commands/msg/archive.go --- aerc-0.13.0/commands/msg/archive.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/archive.go 2023-01-04 15:38:38.000000000 +0000 @@ -5,12 +5,9 @@ "fmt" "path" "sync" - "time" "git.sr.ht/~rjarry/aerc/commands" - "git.sr.ht/~rjarry/aerc/lib" - "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" @@ -104,41 +101,11 @@ } // we need to do that in the background, else we block the main thread go func() { - defer logging.PanicHandler() + defer log.PanicHandler() wg.Wait() if success { - aerc.PushStatus("Messages archived.", 10*time.Second) - mv, isMsgView := h.msgProvider.(*widgets.MessageViewer) - if isMsgView { - if !aerc.Config().Ui.NextMessageOnDelete { - aerc.RemoveTab(h.msgProvider) - } else { - // no more messages in the list - if next == nil { - aerc.RemoveTab(h.msgProvider) - acct.Messages().Select(-1) - ui.Invalidate() - return - } - lib.NewMessageStoreView(next, mv.MessageView().SeenFlagSet(), - store, aerc.Crypto, aerc.DecryptKeys, - func(view lib.MessageView, err error) { - if err != nil { - aerc.PushError(err.Error()) - return - } - nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) - aerc.ReplaceTab(mv, nextMv, next.Envelope.Subject) - }) - } - } else { - if next == nil { - // We archived the last message, select the new last message - // instead of the first message - acct.Messages().Select(-1) - } - } + handleDone(aerc, acct, next, "Messages archived.", store) } }() return nil diff -Nru aerc-0.13.0/commands/msg/delete.go aerc-0.14.0/commands/msg/delete.go --- aerc-0.13.0/commands/msg/delete.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/delete.go 2023-01-04 15:38:38.000000000 +0000 @@ -4,6 +4,7 @@ "errors" "time" + "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/models" @@ -54,7 +55,7 @@ aerc.PushStatus("Messages deleted.", 10*time.Second) mv, isMsgView := h.msgProvider.(*widgets.MessageViewer) if isMsgView { - if !aerc.Config().Ui.NextMessageOnDelete { + if !config.Ui.NextMessageOnDelete { aerc.RemoveTab(h.msgProvider) } else { // no more messages in the list @@ -71,7 +72,7 @@ aerc.PushError(err.Error()) return } - nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) + nextMv := widgets.NewMessageViewer(acct, view) aerc.ReplaceTab(mv, nextMv, next.Envelope.Subject) }) } @@ -109,6 +110,9 @@ } } if next == nil || previous == next { + // If previous == next, this is the last + // message. Set next to nil either way + next = nil break } stepFn() diff -Nru aerc-0.13.0/commands/msg/envelope.go aerc-0.14.0/commands/msg/envelope.go --- aerc-0.13.0/commands/msg/envelope.go 1970-01-01 00:00:00.000000000 +0000 +++ aerc-0.14.0/commands/msg/envelope.go 2023-01-04 15:38:38.000000000 +0000 @@ -0,0 +1,152 @@ +package msg + +import ( + "errors" + "fmt" + "strings" + + "git.sr.ht/~rjarry/aerc/lib/format" + "git.sr.ht/~rjarry/aerc/log" + "git.sr.ht/~rjarry/aerc/models" + "git.sr.ht/~rjarry/aerc/widgets" + "git.sr.ht/~sircmpwn/getopt" + "github.com/emersion/go-message/mail" +) + +type Envelope struct{} + +func init() { + register(Envelope{}) +} + +func (Envelope) Aliases() []string { + return []string{"envelope"} +} + +func (Envelope) Complete(aerc *widgets.Aerc, args []string) []string { + return nil +} + +func (Envelope) Execute(aerc *widgets.Aerc, args []string) error { + header := false + fmtStr := "%-20.20s: %s" + opts, _, err := getopt.Getopts(args, "hs:") + if err != nil { + return err + } + for _, opt := range opts { + switch opt.Option { + case 's': + fmtStr = opt.Value + case 'h': + header = true + } + } + + acct := aerc.SelectedAccount() + if acct == nil { + return errors.New("No account selected") + } + + var list []string + if msg, err := acct.SelectedMessage(); err != nil { + return err + } else { + if msg != nil { + if header { + list = parseHeader(msg, fmtStr) + } else { + list = parseEnvelope(msg, fmtStr, + acct.UiConfig().TimestampFormat) + } + } else { + return fmt.Errorf("Selected message is empty.") + } + } + + n := len(list) + aerc.AddDialog(widgets.NewDialog( + widgets.NewListBox( + "Message Envelope. Press or to close. "+ + "Start typing to filter.", + list, + aerc.SelectedAccountUiConfig(), + func(_ string) { + aerc.CloseDialog() + }, + ), + // start pos on screen + func(h int) int { + if n < h/8*6 { + return h/2 - n/2 - 1 + } + return h / 8 + }, + // dialog height + func(h int) int { + if n < h/8*6 { + return n + 2 + } + return h / 8 * 6 + }, + )) + + return nil +} + +func parseEnvelope(msg *models.MessageInfo, fmtStr, fmtTime string, +) (result []string) { + if envlp := msg.Envelope; envlp != nil { + addStr := func(key, text string) { + result = append(result, fmt.Sprintf(fmtStr, key, text)) + } + addAddr := func(key string, ls []*mail.Address) { + for _, l := range ls { + result = append(result, + fmt.Sprintf(fmtStr, key, + format.AddressForHumans(l))) + } + } + + addStr("Date", envlp.Date.Format(fmtTime)) + addStr("Subject", envlp.Subject) + addStr("Message-Id", envlp.MessageId) + + addAddr("From", envlp.From) + addAddr("To", envlp.To) + addAddr("ReplyTo", envlp.ReplyTo) + addAddr("Cc", envlp.Cc) + addAddr("Bcc", envlp.Bcc) + } + return +} + +func parseHeader(msg *models.MessageInfo, fmtStr string) (result []string) { + if h := msg.RFC822Headers; h != nil { + hf := h.Fields() + for hf.Next() { + text, err := hf.Text() + if err != nil { + log.Errorf(err.Error()) + text = hf.Value() + } + result = append(result, + headerExpand(fmtStr, hf.Key(), text)...) + } + } + return +} + +func headerExpand(fmtStr, key, text string) []string { + var result []string + switch strings.ToLower(key) { + case "to", "from", "bcc", "cc": + for _, item := range strings.Split(text, ",") { + result = append(result, fmt.Sprintf(fmtStr, key, + strings.TrimSpace(item))) + } + default: + result = append(result, fmt.Sprintf(fmtStr, key, text)) + } + return result +} diff -Nru aerc-0.13.0/commands/msg/forward.go aerc-0.14.0/commands/msg/forward.go --- aerc-0.13.0/commands/msg/forward.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/forward.go 2023-01-04 15:38:38.000000000 +0000 @@ -12,10 +12,11 @@ "strings" "sync" + "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" @@ -74,7 +75,7 @@ if err != nil { return err } - logging.Infof("Forwarding email %s", msg.Envelope.MessageId) + log.Debugf("Forwarding email <%s>", msg.Envelope.MessageId) h := &mail.Header{} subject := "Fwd: " + msg.Envelope.Subject @@ -99,7 +100,7 @@ } addTab := func() (*widgets.Composer, error) { - composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), + composer, err := widgets.NewComposer(aerc, acct, acct.AccountConfig(), acct.Worker(), template, h, original) if err != nil { aerc.PushError("Error: " + err.Error()) @@ -133,10 +134,10 @@ store.FetchFull([]uint32{msg.Uid}, func(fm *types.FullMessage) { tmpFile, err := os.Create(tmpFileName) if err != nil { - logging.Warnf("failed to create temporary attachment: %v", err) + log.Warnf("failed to create temporary attachment: %v", err) _, err = addTab() if err != nil { - logging.Warnf("failed to add tab: %v", err) + log.Warnf("failed to add tab: %v", err) } return } @@ -144,7 +145,7 @@ defer tmpFile.Close() _, err = io.Copy(tmpFile, fm.Content.Reader) if err != nil { - logging.Warnf("failed to write to tmpfile: %v", err) + log.Warnf("failed to write to tmpfile: %v", err) return } composer, err := addTab() @@ -158,7 +159,7 @@ }) } else { if template == "" { - template = aerc.Config().Templates.Forwards + template = config.Templates.Forwards } part := lib.FindPlaintext(msg.BodyStructure, nil) @@ -194,7 +195,7 @@ } bs, err := msg.BodyStructure.PartAtIndex(p) if err != nil { - logging.Errorf("cannot get PartAtIndex %v: %v", p, err) + log.Errorf("cannot get PartAtIndex %v: %v", p, err) continue } store.FetchBodyPart(msg.Uid, p, func(reader io.Reader) { @@ -205,8 +206,12 @@ name = fmt.Sprintf("%s_%s_%d", bs.MIMEType, bs.MIMESubType, rand.Uint64()) } mu.Lock() - composer.AddPartAttachment(name, mime, params, reader) + err := composer.AddPartAttachment(name, mime, params, reader) mu.Unlock() + if err != nil { + log.Errorf(err.Error()) + aerc.PushError(err.Error()) + } }) } } diff -Nru aerc-0.13.0/commands/msg/invite.go aerc-0.14.0/commands/msg/invite.go --- aerc-0.13.0/commands/msg/invite.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/invite.go 2023-01-04 15:38:38.000000000 +0000 @@ -5,11 +5,12 @@ "fmt" "io" + "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/calendar" "git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "github.com/emersion/go-message/mail" @@ -48,7 +49,7 @@ return fmt.Errorf("no invitation found (missing text/calendar)") } - subject := trimLocalizedRe(msg.Envelope.Subject) + subject := trimLocalizedRe(msg.Envelope.Subject, acct.AccountConfig().LocalizedRe) switch args[0] { case "accept": subject = "Accepted: " + subject @@ -99,7 +100,7 @@ to = msg.Envelope.From } - if !aerc.Config().Compose.ReplyToSelf { + if !config.Compose.ReplyToSelf { for i, v := range to { if v.Address == from.Address { to = append(to[:i], to[i+1:]...) @@ -147,7 +148,7 @@ } addTab := func(cr *calendar.Reply) error { - composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), + composer, err := widgets.NewComposer(aerc, acct, acct.AccountConfig(), acct.Worker(), "", h, original) if err != nil { aerc.PushError("Error: " + err.Error()) @@ -187,7 +188,7 @@ } else { err := addTab(cr) if err != nil { - logging.Warnf("failed to add tab: %v", err) + log.Warnf("failed to add tab: %v", err) } } }) diff -Nru aerc-0.13.0/commands/msg/move.go aerc-0.14.0/commands/msg/move.go --- aerc-0.13.0/commands/msg/move.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/move.go 2023-01-04 15:38:38.000000000 +0000 @@ -5,11 +5,14 @@ "strings" "time" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/commands" + "git.sr.ht/~rjarry/aerc/config" + "git.sr.ht/~rjarry/aerc/lib" + "git.sr.ht/~rjarry/aerc/lib/ui" + "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" + "git.sr.ht/~sircmpwn/getopt" ) type Move struct{} @@ -42,32 +45,77 @@ } h := newHelper(aerc) + acct, err := h.account() + if err != nil { + return err + } store, err := h.store() if err != nil { return err } - uids, err := h.markedOrSelectedUids() + msgs, err := h.messages() if err != nil { return err } - _, isMsgView := h.msgProvider.(*widgets.MessageViewer) - if isMsgView { - aerc.RemoveTab(h.msgProvider) + var uids []uint32 + for _, msg := range msgs { + uids = append(uids, msg.Uid) } marker := store.Marker() marker.ClearVisualMark() - findNextNonDeleted(uids, store) + next := findNextNonDeleted(uids, store) joinedArgs := strings.Join(args[optind:], " ") + store.Move(uids, joinedArgs, createParents, func( msg types.WorkerMessage, ) { switch msg := msg.(type) { case *types.Done: - aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second) + handleDone(aerc, acct, next, "Messages moved to "+joinedArgs, store) case *types.Error: - marker.Remark() aerc.PushError(msg.Error.Error()) + marker.Remark() } }) + return nil } + +func handleDone( + aerc *widgets.Aerc, + acct *widgets.AccountView, + next *models.MessageInfo, + message string, + store *lib.MessageStore, +) { + h := newHelper(aerc) + aerc.PushStatus(message, 10*time.Second) + mv, isMsgView := h.msgProvider.(*widgets.MessageViewer) + switch { + case isMsgView && !config.Ui.NextMessageOnDelete: + aerc.RemoveTab(h.msgProvider) + case isMsgView: + if next == nil { + aerc.RemoveTab(h.msgProvider) + acct.Messages().Select(-1) + ui.Invalidate() + return + } + lib.NewMessageStoreView(next, mv.MessageView().SeenFlagSet(), + store, aerc.Crypto, aerc.DecryptKeys, + func(view lib.MessageView, err error) { + if err != nil { + aerc.PushError(err.Error()) + return + } + nextMv := widgets.NewMessageViewer(acct, view) + aerc.ReplaceTab(mv, nextMv, next.Envelope.Subject) + }) + default: + if next == nil { + // We moved the last message, select the new last message + // instead of the first message + acct.Messages().Select(-1) + } + } +} diff -Nru aerc-0.13.0/commands/msg/pipe.go aerc-0.14.0/commands/msg/pipe.go --- aerc-0.13.0/commands/msg/pipe.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/pipe.go 2023-01-04 15:38:38.000000000 +0000 @@ -7,11 +7,10 @@ "os/exec" "regexp" "sort" - "strconv" "time" "git.sr.ht/~rjarry/aerc/commands" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" mboxer "git.sr.ht/~rjarry/aerc/worker/mbox" "git.sr.ht/~rjarry/aerc/worker/types" @@ -93,12 +92,12 @@ return } go func() { - defer logging.PanicHandler() + defer log.PanicHandler() defer pipe.Close() _, err := io.Copy(pipe, reader) if err != nil { - logging.Errorf("failed to send data to pipe: %v", err) + log.Errorf("failed to send data to pipe: %v", err) } }() err = ecmd.Run() @@ -124,6 +123,18 @@ h := newHelper(aerc) store, err := h.store() if err != nil { + if mv, ok := provider.(*widgets.MessageViewer); ok { + mv.MessageView().FetchFull(func(reader io.Reader) { + if background { + doExec(reader) + } else { + doTerm(reader, + fmt.Sprintf("%s <%s", + cmd[0], title)) + } + }) + return nil + } return err } uids, err = h.markedOrSelectedUids() @@ -155,7 +166,7 @@ }) go func() { - defer logging.PanicHandler() + defer log.PanicHandler() select { case <-done: @@ -169,11 +180,11 @@ } } - is_git_patches := true + is_git_patches := false for _, msg := range messages { info := store.Messages[msg.Content.Uid] - if info == nil || !gitMessageIdRe.MatchString(info.Envelope.MessageId) { - is_git_patches = false + if info != nil && patchSeriesRe.MatchString(info.Envelope.Subject) { + is_git_patches = true break } } @@ -186,9 +197,7 @@ if infoi == nil || infoj == nil { return false } - msgidi := padGitMessageId(infoi.Envelope.MessageId) - msgidj := padGitMessageId(infoj.Envelope.MessageId) - return msgidi < msgidj + return infoi.Envelope.Subject < infoj.Envelope.Subject }) } @@ -218,7 +227,9 @@ } }) } - provider.Store().Marker().ClearVisualMark() + if store := provider.Store(); store != nil { + store.Marker().ClearVisualMark() + } return nil } @@ -234,28 +245,13 @@ _, err = io.Copy(pw, msg.Content.Reader) } if err != nil { - logging.Warnf("failed to write data: %v", err) + log.Warnf("failed to write data: %v", err) } } }() return pr } -var gitMessageIdRe = regexp.MustCompile(`^(\d+\.\d+)-(\d+)-(.+)$`) - -// Git send-email Message-Id headers have the following format: -// -// DATETIME.PID-NUM-COMMITTER -// -// Return a copy of the message id with NUM zero-padded to three characters. -func padGitMessageId(msgId string) string { - matches := gitMessageIdRe.FindStringSubmatch(msgId) - if matches == nil { - return msgId - } - number, err := strconv.Atoi(matches[2]) - if err != nil { - return msgId - } - return fmt.Sprintf("%s-%03d-%s", matches[1], number, matches[3]) -} +var patchSeriesRe = regexp.MustCompile( + `^.*\[(RFC )?PATCH( [^\]]+)? \d+/\d+] .+$`, +) diff -Nru aerc-0.13.0/commands/msg/recall.go aerc-0.14.0/commands/msg/recall.go --- aerc-0.13.0/commands/msg/recall.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/recall.go 2023-01-04 15:38:38.000000000 +0000 @@ -14,7 +14,7 @@ "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "git.sr.ht/~rjarry/aerc/worker/types" @@ -70,9 +70,9 @@ if err != nil { return errors.Wrap(err, "Recall failed") } - logging.Infof("Recalling message %s", msgInfo.Envelope.MessageId) + log.Debugf("Recalling message <%s>", msgInfo.Envelope.MessageId) - composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), + composer, err := widgets.NewComposer(aerc, acct, acct.AccountConfig(), acct.Worker(), "", msgInfo.RFC822Headers, models.OriginalMail{}) if err != nil { @@ -185,7 +185,7 @@ if md.IsSigned { err = composer.SetSign(md.IsSigned) if err != nil { - logging.Warnf("failed to set signed state: %v", err) + log.Warnf("failed to set signed state: %v", err) } } } @@ -200,7 +200,7 @@ } bs, err := msg.BodyStructure().PartAtIndex(p) if err != nil { - logging.Infof("cannot get PartAtIndex %v: %v", p, err) + log.Warnf("cannot get PartAtIndex %v: %v", p, err) continue } msg.FetchBodyPart(p, func(reader io.Reader) { @@ -211,8 +211,12 @@ name = fmt.Sprintf("%s_%s_%d", bs.MIMEType, bs.MIMESubType, rand.Uint64()) } mu.Lock() - composer.AddPartAttachment(name, mime, params, reader) + err := composer.AddPartAttachment(name, mime, params, reader) mu.Unlock() + if err != nil { + log.Errorf(err.Error()) + aerc.PushError(err.Error()) + } }) } }) diff -Nru aerc-0.13.0/commands/msg/reply.go aerc-0.14.0/commands/msg/reply.go --- aerc-0.13.0/commands/msg/reply.go 2022-10-20 20:21:09.000000000 +0000 +++ aerc-0.14.0/commands/msg/reply.go 2023-01-04 15:38:38.000000000 +0000 @@ -10,11 +10,13 @@ "git.sr.ht/~sircmpwn/getopt" + "git.sr.ht/~rjarry/aerc/commands/account" + "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/format" "git.sr.ht/~rjarry/aerc/lib/ui" - "git.sr.ht/~rjarry/aerc/logging" + "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/widgets" "github.com/emersion/go-message/mail" @@ -35,22 +37,25 @@ } func (reply) Execute(aerc *widgets.Aerc, args []string) error { - opts, optind, err := getopt.Getopts(args, "aqT:") + opts, optind, err := getopt.Getopts(args, "acqT:") if err != nil { return err } if optind != len(args) { - return errors.New("Usage: reply [-aq -T