package compose import ( "bufio" "bytes" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "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/lib/xdg" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/widgets" "github.com/pkg/errors" "git.sr.ht/~sircmpwn/getopt" ) type Attach struct{} func init() { register(Attach{}) } func (Attach) Aliases() []string { return []string{"attach"} } func (Attach) Complete(aerc *widgets.Aerc, args []string) []string { path := strings.Join(args, " ") return commands.CompletePath(path) } func (a Attach) Execute(aerc *widgets.Aerc, args []string) error { var ( menu bool read bool ) opts, optind, err := getopt.Getopts(args, "mr") if err != nil { return err } for _, opt := range opts { switch opt.Option { case 'm': if read { return errors.New("-m and -r are mutually exclusive") } menu = true case 'r': if menu { return errors.New("-m and -r are mutually exclusive") } read = true } } args = args[optind:] if menu { return a.openMenu(aerc, args) } if read { if len(args) < 2 { return fmt.Errorf("Usage: :attach -r [args...]") } return a.readCommand(aerc, args[0], args[1:]) } if len(args) == 0 { return fmt.Errorf("Usage: :attach ") } return a.addPath(aerc, strings.Join(args, " ")) } func (a Attach) addPath(aerc *widgets.Aerc, path string) error { path = xdg.ExpandHome(path) attachments, err := filepath.Glob(path) if err != nil && errors.Is(err, filepath.ErrBadPattern) { log.Warnf("failed to parse as globbing pattern: %v", err) attachments = []string{path} } 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 { log.Debugf("attaching '%s'", attach) pathinfo, err := os.Stat(attach) if err != nil { log.Errorf("failed to stat file: %v", err) aerc.PushError(err.Error()) return err } else if pathinfo.IsDir() && len(attachments) == 1 { aerc.PushError("Attachment must be a file, not a directory") return nil } composer.AddAttachment(attach) } if len(attachments) == 1 { aerc.PushSuccess(fmt.Sprintf("Attached %s", path)) } else { aerc.PushSuccess(fmt.Sprintf("Attached %d files", len(attachments))) } 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 } var filepicker *exec.Cmd if strings.Contains(filePickerCmd, "%f") { filePickerCmd = strings.ReplaceAll(filePickerCmd, "%f", picks.Name()) filepicker = exec.Command("sh", "-c", filePickerCmd) } else { filepicker = exec.Command("sh", "-c", filePickerCmd+" >&3") filepicker.ExtraFiles = append(filepicker.ExtraFiles, picks) } t, err := widgets.NewTerminal(filepicker) if err != nil { return err } t.Focus(true) 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 } func (a Attach) readCommand(aerc *widgets.Aerc, name string, args []string) error { args = append([]string{"-c"}, args...) cmd := exec.Command("sh", args...) data, err := cmd.Output() if err != nil { return errors.Wrap(err, "Output") } reader := bufio.NewReader(bytes.NewReader(data)) mimeType, mimeParams, err := lib.FindMimeType(name, reader) if err != nil { return errors.Wrap(err, "FindMimeType") } mimeParams["name"] = name composer, _ := aerc.SelectedTabContent().(*widgets.Composer) err = composer.AddPartAttachment(name, mimeType, mimeParams, reader) if err != nil { return errors.Wrap(err, "AddPartAttachment") } aerc.PushSuccess(fmt.Sprintf("Attached %s", name)) return nil }