init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
package msgview
import (
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type NextPrevPart struct {
Offset int `opt:"n" default:"1"`
}
func init() {
commands.Register(NextPrevPart{})
}
func (NextPrevPart) Description() string {
return "Cycle between message parts being shown."
}
func (NextPrevPart) Context() commands.CommandContext {
return commands.MESSAGE_VIEWER
}
func (NextPrevPart) Aliases() []string {
return []string{"next-part", "prev-part"}
}
func (np NextPrevPart) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
for n := 0; n < np.Offset; n++ {
if args[0] == "prev-part" {
mv.PreviousPart()
} else {
mv.NextPart()
}
}
return nil
}
+62
View File
@@ -0,0 +1,62 @@
package msgview
import (
"fmt"
"net/url"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/log"
)
type OpenLink struct {
Url *url.URL `opt:"url" action:"ParseUrl" complete:"CompleteUrl"`
Cmd string `opt:"..." required:"false"`
}
func init() {
commands.Register(OpenLink{})
}
func (OpenLink) Description() string {
return "Open the specified URL with an external program."
}
func (OpenLink) Context() commands.CommandContext {
return commands.MESSAGE_VIEWER
}
func (OpenLink) Aliases() []string {
return []string{"open-link"}
}
func (*OpenLink) CompleteUrl(arg string) []string {
mv := app.SelectedTabContent().(*app.MessageViewer)
if mv != nil {
if p := mv.SelectedMessagePart(); p != nil {
return commands.FilterList(p.Links, arg, nil)
}
}
return nil
}
func (o *OpenLink) ParseUrl(arg string) error {
u, err := url.Parse(arg)
if err != nil {
return err
}
o.Url = u
return nil
}
func (o OpenLink) Execute(args []string) error {
mime := fmt.Sprintf("x-scheme-handler/%s", o.Url.Scheme)
go func() {
defer log.PanicHandler()
if err := lib.XDGOpenMime(o.Url.String(), mime, o.Cmd); err != nil {
app.PushError("open-link: " + err.Error())
}
}()
return nil
}
+96
View File
@@ -0,0 +1,96 @@
package msgview
import (
"errors"
"io"
"mime"
"os"
"path"
"path/filepath"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/log"
)
type Open struct {
Delete bool `opt:"-d" desc:"Delete temp file after the opener exits."`
Cmd string `opt:"..." required:"false"`
}
func init() {
commands.Register(Open{})
}
func (Open) Description() string {
return "Save the current message part to a temporary file, then open it."
}
func (Open) Context() commands.CommandContext {
return commands.MESSAGE_VIEWER
}
func (Open) Aliases() []string {
return []string{"open"}
}
func (o Open) Execute(args []string) error {
mv := app.SelectedTabContent().(*app.MessageViewer)
if mv == nil {
return errors.New("open only supported selected message parts")
}
p := mv.SelectedMessagePart()
mv.MessageView().FetchBodyPart(p.Index, func(reader io.Reader) {
mimeType := ""
part, err := mv.MessageView().BodyStructure().PartAtIndex(p.Index)
if err != nil {
app.PushError(err.Error())
return
}
mimeType = part.FullMIMEType()
tmpDir, err := os.MkdirTemp(os.TempDir(), "aerc-*")
if err != nil {
app.PushError(err.Error())
return
}
filename := path.Base(part.FileName())
var tmpFile *os.File
if filename == "." {
extension := ""
if exts, _ := mime.ExtensionsByType(mimeType); len(exts) > 0 {
extension = exts[0]
}
tmpFile, err = os.CreateTemp(tmpDir, "aerc-*"+extension)
} else {
tmpFile, err = os.Create(filepath.Join(tmpDir, filename))
}
if err != nil {
app.PushError(err.Error())
return
}
_, err = io.Copy(tmpFile, reader)
tmpFile.Close()
if err != nil {
app.PushError(err.Error())
return
}
go func() {
defer log.PanicHandler()
if o.Delete {
defer os.RemoveAll(tmpDir)
}
err = lib.XDGOpenMime(tmpFile.Name(), mimeType, o.Cmd)
if err != nil {
app.PushError("open: " + err.Error())
}
}()
})
return nil
}
+210
View File
@@ -0,0 +1,210 @@
package msgview
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/xdg"
"git.sr.ht/~rjarry/aerc/models"
)
type Save struct {
Force bool `opt:"-f" desc:"Overwrite destination path."`
CreateDirs bool `opt:"-p" desc:"Create missing directories."`
Attachments bool `opt:"-a" desc:"Save all attachments parts."`
AllAttachments bool `opt:"-A" desc:"Save all named parts."`
Path string `opt:"path" required:"false" complete:"CompletePath" desc:"Target file path."`
}
func init() {
commands.Register(Save{})
}
func (Save) Description() string {
return "Save the current message part to the given path."
}
func (Save) Context() commands.CommandContext {
return commands.MESSAGE_VIEWER
}
func (Save) Aliases() []string {
return []string{"save"}
}
func (*Save) CompletePath(arg string) []string {
defaultPath := config.General.DefaultSavePath
if defaultPath != "" && !isAbsPath(arg) {
arg = filepath.Join(defaultPath, arg)
}
return commands.CompletePath(arg, false)
}
func (s Save) Execute(args []string) error {
// we either need a path or a defaultPath
if s.Path == "" && config.General.DefaultSavePath == "" {
return errors.New("No default save path in config")
}
// Absolute paths are taken as is so that the user can override the default
// if they want to
if !isAbsPath(s.Path) {
s.Path = filepath.Join(config.General.DefaultSavePath, s.Path)
}
s.Path = xdg.ExpandHome(s.Path)
mv, ok := app.SelectedTabContent().(*app.MessageViewer)
if !ok {
return fmt.Errorf("SelectedTabContent is not a MessageViewer")
}
if s.Attachments || s.AllAttachments {
parts := mv.AttachmentParts(s.AllAttachments)
if len(parts) == 0 {
return fmt.Errorf("This message has no attachments")
}
names := make(map[string]struct{})
for _, pi := range parts {
if err := s.savePart(pi, mv, names); err != nil {
return err
}
}
return nil
}
pi := mv.SelectedMessagePart()
return s.savePart(pi, mv, make(map[string]struct{}))
}
func (s *Save) savePart(
pi *app.PartInfo,
mv *app.MessageViewer,
names map[string]struct{},
) error {
path := s.Path
if s.Attachments || s.AllAttachments || isDirExists(path) {
filename := generateFilename(pi.Part)
path = filepath.Join(path, filename)
}
dir := filepath.Dir(path)
if s.CreateDirs && dir != "" {
err := os.MkdirAll(dir, 0o755)
if err != nil {
return err
}
}
path = getCollisionlessFilename(path, names)
names[path] = struct{}{}
if pathExists(path) && !s.Force {
return fmt.Errorf("%q already exists and -f not given", path)
}
ch := make(chan error, 1)
mv.MessageView().FetchBodyPart(pi.Index, func(reader io.Reader) {
f, err := os.Create(path)
if err != nil {
ch <- err
return
}
defer f.Close()
_, err = io.Copy(f, reader)
if err != nil {
ch <- err
return
}
ch <- nil
})
// we need to wait for the callback prior to displaying a result
go func() {
defer log.PanicHandler()
err := <-ch
if err != nil {
app.PushError(fmt.Sprintf("Save failed: %v", err))
return
}
app.PushStatus("Saved to "+path, 10*time.Second)
}()
return nil
}
func getCollisionlessFilename(path string, existing map[string]struct{}) string {
ext := filepath.Ext(path)
name := strings.TrimSuffix(path, ext)
_, exists := existing[path]
counter := 1
for exists {
path = fmt.Sprintf("%s_%d%s", name, counter, ext)
counter++
_, exists = existing[path]
}
return path
}
// isDir returns true if path is a directory and exists
func isDirExists(path string) bool {
pathinfo, err := os.Stat(path)
if err != nil {
return false // we don't really care
}
if pathinfo.IsDir() {
return true
}
return false
}
// pathExists returns true if path exists
func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// isAbsPath returns true if path given is anchored to / or . or ~
func isAbsPath(path string) bool {
if len(path) == 0 {
return false
}
switch path[0] {
case '/':
return true
case '.':
return true
case '~':
return true
default:
return false
}
}
// generateFilename tries to get the filename from the given part.
// if that fails it will fallback to a generated one based on the date
func generateFilename(part *models.BodyStructure) string {
filename := part.FileName()
// Some MUAs send attachments with names like /some/stupid/idea/happy.jpeg
// Assuming non hostile intent it does make sense to use just the last
// portion of the pathname as the filename for saving it.
filename = filename[strings.LastIndex(filename, "/")+1:]
switch filename {
case "", ".", "..":
timestamp := time.Now().Format("2006-01-02-150405")
filename = fmt.Sprintf("aerc_%v", timestamp)
default:
// already have a valid name
}
return filename
}
+24
View File
@@ -0,0 +1,24 @@
package msgview
import "testing"
func TestGetCollisionlessFilename(t *testing.T) {
tests := []struct {
originalFilename string
expectedNewName string
existingFiles map[string]struct{}
}{
{"test", "test", map[string]struct{}{}},
{"test", "test", map[string]struct{}{"other-file": {}}},
{"test.txt", "test.txt", map[string]struct{}{"test.log": {}}},
{"test.txt", "test_1.txt", map[string]struct{}{"test.txt": {}}},
{"test.txt", "test_2.txt", map[string]struct{}{"test.txt": {}, "test_1.txt": {}}},
{"test.txt", "test_1.txt", map[string]struct{}{"test.txt": {}, "test_2.txt": {}}},
}
for _, tt := range tests {
actual := getCollisionlessFilename(tt.originalFilename, tt.existingFiles)
if actual != tt.expectedNewName {
t.Errorf("expected %s, actual %s", tt.expectedNewName, actual)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
package msgview
import (
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type ToggleHeaders struct{}
func init() {
commands.Register(ToggleHeaders{})
}
func (ToggleHeaders) Description() string {
return "Toggle the visibility of message headers."
}
func (ToggleHeaders) Context() commands.CommandContext {
return commands.MESSAGE_VIEWER
}
func (ToggleHeaders) Aliases() []string {
return []string{"toggle-headers"}
}
func (ToggleHeaders) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
mv.ToggleHeaders()
return nil
}
@@ -0,0 +1,34 @@
package msgview
import (
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/state"
)
type ToggleKeyPassthrough struct{}
func init() {
commands.Register(ToggleKeyPassthrough{})
}
func (ToggleKeyPassthrough) Description() string {
return "Enter or exit the passthrough key bindings context."
}
func (ToggleKeyPassthrough) Context() commands.CommandContext {
return commands.MESSAGE_VIEWER
}
func (ToggleKeyPassthrough) Aliases() []string {
return []string{"toggle-key-passthrough"}
}
func (ToggleKeyPassthrough) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
keyPassthroughEnabled := mv.ToggleKeyPassthrough()
if acct := mv.SelectedAccount(); acct != nil {
acct.SetStatus(state.Passthrough(keyPassthroughEnabled))
}
return nil
}